diff --git a/packages/parser/README.md b/packages/parser/README.md index 0599f61b236..433c175bfef 100644 --- a/packages/parser/README.md +++ b/packages/parser/README.md @@ -64,6 +64,8 @@ interface ParserOptions { tsconfigRootDir?: string; extraFileExtensions?: string[]; warnOnUnsupportedTypeScriptVersion?: boolean; + + program?: import('typescript').Program; } ``` @@ -211,6 +213,39 @@ Default `false`. This option allows you to request that when the `project` setting is specified, files will be allowed when not included in the projects defined by the provided `tsconfig.json` files. **Using this option will incur significant performance costs. This option is primarily included for backwards-compatibility.** See the **`project`** section above for more information. +### `parserOptions.program` + +Default `undefined`. + +This option allows you to programmatically provide an instance of a TypeScript Program object that will provide type information to rules. +This will override any program that would have been computed from `parserOptions.project` or `parserOptions.createDefaultProgram`. +All linted files must be part of the provided program. + +## Utilities + +### `createProgram(configFile, projectDirectory)` + +This serves as a utility method for users of the `parserOptions.program` feature to create a TypeScript program instance from a config file. + +```ts +declare function createProgram( + configFile: string, + projectDirectory?: string, +): import('typescript').Program; +``` + +Example usage in .eslintrc.js: + +```js +const parser = require('@typescript-eslint/parser'); +const program = parser.createProgram('tsconfig.json'); +module.exports = { + parserOptions: { + program, + }, +}; +``` + ## Supported TypeScript Version Please see [`typescript-eslint`](https://github.com/typescript-eslint/typescript-eslint) for the supported TypeScript version. diff --git a/packages/parser/src/index.ts b/packages/parser/src/index.ts index 451f263d62b..c1bb82bf593 100644 --- a/packages/parser/src/index.ts +++ b/packages/parser/src/index.ts @@ -2,6 +2,7 @@ export { parse, parseForESLint, ParserOptions } from './parser'; export { ParserServices, clearCaches, + createProgram, } from '@typescript-eslint/typescript-estree'; // note - cannot migrate this to an import statement because it will make TSC copy the package.json to the dist folder diff --git a/packages/parser/tests/lib/services.ts b/packages/parser/tests/lib/services.ts index 28cada9d251..4567f5f85a4 100644 --- a/packages/parser/tests/lib/services.ts +++ b/packages/parser/tests/lib/services.ts @@ -7,6 +7,7 @@ import { formatSnapshotName, testServices, } from '../tools/test-utils'; +import { createProgram } from '@typescript-eslint/typescript-estree'; //------------------------------------------------------------------------------ // Setup @@ -30,15 +31,17 @@ function createConfig(filename: string): ParserOptions { //------------------------------------------------------------------------------ describe('services', () => { + const program = createProgram(path.resolve(FIXTURES_DIR, 'tsconfig.json')); testFiles.forEach(filename => { const code = fs.readFileSync(path.join(FIXTURES_DIR, filename), 'utf8'); const config = createConfig(filename); - it( - formatSnapshotName(filename, FIXTURES_DIR, '.ts'), - createSnapshotTestBlock(code, config), - ); - it(`${formatSnapshotName(filename, FIXTURES_DIR, '.ts')} services`, () => { + const snapshotName = formatSnapshotName(filename, FIXTURES_DIR, '.ts'); + it(snapshotName, createSnapshotTestBlock(code, config)); + it(`${snapshotName} services`, () => { testServices(code, config); }); + it(`${snapshotName} services with provided program`, () => { + testServices(code, { ...config, program }); + }); }); }); diff --git a/packages/types/package.json b/packages/types/package.json index e6a4c44ea3a..88f7555d062 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -48,5 +48,8 @@ "_ts3.4/*" ] } + }, + "devDependencies": { + "typescript": "*" } } diff --git a/packages/types/src/parser-options.ts b/packages/types/src/parser-options.ts index 8f6632df9a4..a8ecb896726 100644 --- a/packages/types/src/parser-options.ts +++ b/packages/types/src/parser-options.ts @@ -1,4 +1,5 @@ import { Lib } from './lib'; +import type { Program } from 'typescript'; type DebugLevel = boolean | ('typescript-eslint' | 'eslint' | 'typescript')[]; @@ -41,6 +42,7 @@ interface ParserOptions { extraFileExtensions?: string[]; filePath?: string; loc?: boolean; + program?: Program; project?: string | string[]; projectFolderIgnoreList?: (string | RegExp)[]; range?: boolean; diff --git a/packages/typescript-estree/README.md b/packages/typescript-estree/README.md index b9d40fb1ec0..fca834e2209 100644 --- a/packages/typescript-estree/README.md +++ b/packages/typescript-estree/README.md @@ -208,6 +208,13 @@ interface ParseAndGenerateServicesOptions extends ParseOptions { */ tsconfigRootDir?: string; + /** + * Instance of a TypeScript Program object to be used for type information. + * This overrides any program or programs that would have been computed from the `project` option. + * All linted files must be part of the provided program. + */ + program?: import('typescript').Program; + /** *************************************************************************************** * IT IS RECOMMENDED THAT YOU DO NOT USE THIS OPTION, AS IT CAUSES PERFORMANCE ISSUES. * @@ -303,6 +310,34 @@ Types for the AST produced by the parse functions. - `AST_NODE_TYPES` is an enum which provides the values for every single AST node's `type` property. - `AST_TOKEN_TYPES` is an enum which provides the values for every single AST token's `type` property. +### Utilities + +#### `createProgram(configFile, projectDirectory)` + +This serves as a utility method for users of the `ParseOptions.program` feature to create a TypeScript program instance from a config file. + +```ts +declare function createProgram( + configFile: string, + projectDirectory: string = process.cwd(), +): import('typescript').Program; +``` + +Example usage: + +```js +const tsESTree = require('@typescript-eslint/typescript-estree'); + +const program = tsESTree.createProgram('tsconfig.json'); +const code = `const hello: string = 'world';`; +const { ast, services } = parseAndGenerateServices(code, { + filePath: '/some/path/to/file/foo.ts', + loc: true, + program, + range: true, +}); +``` + ## Supported TypeScript Version See the [Supported TypeScript Version](../../README.md#supported-typescript-version) section in the project root. diff --git a/packages/typescript-estree/src/create-program/createProjectProgram.ts b/packages/typescript-estree/src/create-program/createProjectProgram.ts index bca6fda1005..69903a5bcaf 100644 --- a/packages/typescript-estree/src/create-program/createProjectProgram.ts +++ b/packages/typescript-estree/src/create-program/createProjectProgram.ts @@ -3,19 +3,12 @@ import path from 'path'; import { getProgramsForProjects } from './createWatchProgram'; import { firstDefined } from '../node-utils'; import { Extra } from '../parser-options'; -import { ASTAndProgram } from './shared'; +import { ASTAndProgram, getAstFromProgram } from './shared'; const log = debug('typescript-eslint:typescript-estree:createProjectProgram'); const DEFAULT_EXTRA_FILE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx']; -function getExtension(fileName: string | undefined): string | null { - if (!fileName) { - return null; - } - return fileName.endsWith('.d.ts') ? '.d.ts' : path.extname(fileName); -} - /** * @param code The code of the file being linted * @param createDefaultProgram True if the default program should be created @@ -31,18 +24,7 @@ function createProjectProgram( const astAndProgram = firstDefined( getProgramsForProjects(code, extra.filePath, extra), - currentProgram => { - const ast = currentProgram.getSourceFile(extra.filePath); - - // working around https://github.com/typescript-eslint/typescript-eslint/issues/1573 - const expectedExt = getExtension(extra.filePath); - const returnedExt = getExtension(ast?.fileName); - if (expectedExt !== returnedExt) { - return; - } - - return ast && { ast, program: currentProgram }; - }, + currentProgram => getAstFromProgram(currentProgram, extra), ); if (!astAndProgram && !createDefaultProgram) { diff --git a/packages/typescript-estree/src/create-program/shared.ts b/packages/typescript-estree/src/create-program/shared.ts index ae296252804..8202c538d34 100644 --- a/packages/typescript-estree/src/create-program/shared.ts +++ b/packages/typescript-estree/src/create-program/shared.ts @@ -1,5 +1,6 @@ import path from 'path'; import * as ts from 'typescript'; +import { Program } from 'typescript'; import { Extra } from '../parser-options'; interface ASTAndProgram { @@ -8,14 +9,11 @@ interface ASTAndProgram { } /** - * Default compiler options for program generation from single root file + * Compiler options required to avoid critical functionality issues */ -const DEFAULT_COMPILER_OPTIONS: ts.CompilerOptions = { - allowNonTsExtensions: true, - allowJs: true, - checkJs: true, - noEmit: true, - // extendedDiagnostics: true, +const CORE_COMPILER_OPTIONS: ts.CompilerOptions = { + noEmit: true, // required to avoid parse from causing emit to occur + /** * Flags required to make no-unused-vars work */ @@ -23,6 +21,16 @@ const DEFAULT_COMPILER_OPTIONS: ts.CompilerOptions = { noUnusedParameters: true, }; +/** + * Default compiler options for program generation + */ +const DEFAULT_COMPILER_OPTIONS: ts.CompilerOptions = { + ...CORE_COMPILER_OPTIONS, + allowNonTsExtensions: true, + allowJs: true, + checkJs: true, +}; + function createDefaultCompilerOptionsFromExtra( extra: Extra, ): ts.CompilerOptions { @@ -93,12 +101,37 @@ function getScriptKind( } } +function getExtension(fileName: string | undefined): string | null { + if (!fileName) { + return null; + } + return fileName.endsWith('.d.ts') ? '.d.ts' : path.extname(fileName); +} + +function getAstFromProgram( + currentProgram: Program, + extra: Extra, +): ASTAndProgram | undefined { + const ast = currentProgram.getSourceFile(extra.filePath); + + // working around https://github.com/typescript-eslint/typescript-eslint/issues/1573 + const expectedExt = getExtension(extra.filePath); + const returnedExt = getExtension(ast?.fileName); + if (expectedExt !== returnedExt) { + return undefined; + } + + return ast && { ast, program: currentProgram }; +} + export { ASTAndProgram, + CORE_COMPILER_OPTIONS, canonicalDirname, CanonicalPath, createDefaultCompilerOptionsFromExtra, ensureAbsolutePath, getCanonicalFileName, getScriptKind, + getAstFromProgram, }; diff --git a/packages/typescript-estree/src/create-program/useProvidedProgram.ts b/packages/typescript-estree/src/create-program/useProvidedProgram.ts new file mode 100644 index 00000000000..16aebbf8f69 --- /dev/null +++ b/packages/typescript-estree/src/create-program/useProvidedProgram.ts @@ -0,0 +1,87 @@ +import debug from 'debug'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as ts from 'typescript'; +import { Extra } from '../parser-options'; +import { + ASTAndProgram, + CORE_COMPILER_OPTIONS, + getAstFromProgram, +} from './shared'; + +const log = debug('typescript-eslint:typescript-estree:useProvidedProgram'); + +function useProvidedProgram( + programInstance: ts.Program, + extra: Extra, +): ASTAndProgram | undefined { + log('Retrieving ast for %s from provided program instance', extra.filePath); + + programInstance.getTypeChecker(); // ensure parent pointers are set in source files + + const astAndProgram = getAstFromProgram(programInstance, extra); + + if (!astAndProgram) { + const relativeFilePath = path.relative( + extra.tsconfigRootDir || process.cwd(), + extra.filePath, + ); + const errorLines = [ + '"parserOptions.program" has been provided for @typescript-eslint/parser.', + `The file was not found in the provided program instance: ${relativeFilePath}`, + ]; + + throw new Error(errorLines.join('\n')); + } + + return astAndProgram; +} + +/** + * Utility offered by parser to help consumers construct their own program instance. + * + * @param configFile the path to the tsconfig.json file, relative to `projectDirectory` + * @param projectDirectory the project directory to use as the CWD, defaults to `process.cwd()` + */ +function createProgramFromConfigFile( + configFile: string, + projectDirectory?: string, +): ts.Program { + if (ts.sys === undefined) { + throw new Error( + '`createProgramFromConfigFile` is only supported in a Node-like environment.', + ); + } + + const parsed = ts.getParsedCommandLineOfConfigFile( + configFile, + CORE_COMPILER_OPTIONS, + { + onUnRecoverableConfigFileDiagnostic: diag => { + throw new Error(formatDiagnostics([diag])); // ensures that `parsed` is defined. + }, + fileExists: fs.existsSync, + getCurrentDirectory: () => + (projectDirectory && path.resolve(projectDirectory)) || process.cwd(), + readDirectory: ts.sys.readDirectory, + readFile: file => fs.readFileSync(file, 'utf-8'), + useCaseSensitiveFileNames: ts.sys.useCaseSensitiveFileNames, + }, + ); + const result = parsed!; // parsed is not undefined, since we throw on failure. + if (result.errors.length) { + throw new Error(formatDiagnostics(result.errors)); + } + const host = ts.createCompilerHost(result.options, true); + return ts.createProgram(result.fileNames, result.options, host); +} + +function formatDiagnostics(diagnostics: ts.Diagnostic[]): string | undefined { + return ts.formatDiagnostics(diagnostics, { + getCanonicalFileName: f => f, + getCurrentDirectory: process.cwd, + getNewLine: () => '\n', + }); +} + +export { useProvidedProgram, createProgramFromConfigFile }; diff --git a/packages/typescript-estree/src/index.ts b/packages/typescript-estree/src/index.ts index 81f3d69446b..c80cd8f9de5 100644 --- a/packages/typescript-estree/src/index.ts +++ b/packages/typescript-estree/src/index.ts @@ -3,6 +3,7 @@ export { ParserServices, TSESTreeOptions } from './parser-options'; export { simpleTraverse } from './simple-traverse'; export * from './ts-estree'; export { clearCaches } from './create-program/createWatchProgram'; +export { createProgramFromConfigFile as createProgram } from './create-program/useProvidedProgram'; // re-export for backwards-compat export { visitorKeys } from '@typescript-eslint/visitor-keys'; diff --git a/packages/typescript-estree/src/parser-options.ts b/packages/typescript-estree/src/parser-options.ts index a3758fffb79..3915d5945eb 100644 --- a/packages/typescript-estree/src/parser-options.ts +++ b/packages/typescript-estree/src/parser-options.ts @@ -20,6 +20,7 @@ export interface Extra { loc: boolean; log: (message: string) => void; preserveNodeMaps?: boolean; + program: null | Program; projects: CanonicalPath[]; range: boolean; strict: boolean; @@ -169,6 +170,13 @@ interface ParseAndGenerateServicesOptions extends ParseOptions { */ tsconfigRootDir?: string; + /** + * Instance of a TypeScript Program object to be used for type information. + * This overrides any program or programs that would have been computed from the `project` option. + * All linted files must be part of the provided program. + */ + program?: Program; + /** *************************************************************************************** * IT IS RECOMMENDED THAT YOU DO NOT USE THIS OPTION, AS IT CAUSES PERFORMANCE ISSUES. * diff --git a/packages/typescript-estree/src/parser.ts b/packages/typescript-estree/src/parser.ts index fdbeaa6fa06..8d6d7df312f 100644 --- a/packages/typescript-estree/src/parser.ts +++ b/packages/typescript-estree/src/parser.ts @@ -18,6 +18,8 @@ import { ensureAbsolutePath, getCanonicalFileName, } from './create-program/shared'; +import { Program } from 'typescript'; +import { useProvidedProgram } from './create-program/useProvidedProgram'; const log = debug('typescript-eslint:typescript-estree:parser'); @@ -61,10 +63,12 @@ function enforceString(code: unknown): string { */ function getProgramAndAST( code: string, + programInstance: Program | null, shouldProvideParserServices: boolean, shouldCreateDefaultProgram: boolean, ): ASTAndProgram { return ( + (programInstance && useProvidedProgram(programInstance, extra)) || (shouldProvideParserServices && createProjectProgram(code, shouldCreateDefaultProgram, extra)) || (shouldProvideParserServices && @@ -105,6 +109,7 @@ function resetExtra(): void { loc: false, log: console.log, // eslint-disable-line no-console preserveNodeMaps: true, + program: null, projects: [], range: false, strict: false, @@ -264,22 +269,32 @@ function applyParserOptionsToExtra(options: TSESTreeOptions): void { // NOTE - ensureAbsolutePath relies upon having the correct tsconfigRootDir in extra extra.filePath = ensureAbsolutePath(extra.filePath, extra); - const projectFolderIgnoreList = ( - options.projectFolderIgnoreList ?? ['**/node_modules/**'] - ) - .reduce((acc, folder) => { - if (typeof folder === 'string') { - acc.push(folder); - } - return acc; - }, []) - // prefix with a ! for not match glob - .map(folder => (folder.startsWith('!') ? folder : `!${folder}`)); - // NOTE - prepareAndTransformProjects relies upon having the correct tsconfigRootDir in extra - extra.projects = prepareAndTransformProjects( - options.project, - projectFolderIgnoreList, - ); + if (options.program && typeof options.program === 'object') { + extra.program = options.program; + log( + 'parserOptions.program was provided, so parserOptions.project will be ignored.', + ); + } + + if (!extra.program) { + // providing a program overrides project resolution + const projectFolderIgnoreList = ( + options.projectFolderIgnoreList ?? ['**/node_modules/**'] + ) + .reduce((acc, folder) => { + if (typeof folder === 'string') { + acc.push(folder); + } + return acc; + }, []) + // prefix with a ! for not match glob + .map(folder => (folder.startsWith('!') ? folder : `!${folder}`)); + // NOTE - prepareAndTransformProjects relies upon having the correct tsconfigRootDir in extra + extra.projects = prepareAndTransformProjects( + options.project, + projectFolderIgnoreList, + ); + } if ( Array.isArray(options.extraFileExtensions) && @@ -446,13 +461,13 @@ function parseAndGenerateServices( warnAboutTSVersion(); /** - * Generate a full ts.Program in order to be able to provide parser - * services, such as type-checking + * Generate a full ts.Program or offer provided instance in order to be able to provide parser services, such as type-checking */ const shouldProvideParserServices = - extra.projects && extra.projects.length > 0; + extra.program != null || (extra.projects && extra.projects.length > 0); const { ast, program } = getProgramAndAST( code, + extra.program, shouldProvideParserServices, extra.createDefaultProgram, )!; diff --git a/packages/typescript-estree/tests/lib/semanticInfo.test.ts b/packages/typescript-estree/tests/lib/semanticInfo.test.ts index 3d8cb41bd25..fb6a4cf0e23 100644 --- a/packages/typescript-estree/tests/lib/semanticInfo.test.ts +++ b/packages/typescript-estree/tests/lib/semanticInfo.test.ts @@ -1,6 +1,6 @@ -import { readFileSync } from 'fs'; +import * as fs from 'fs'; import glob from 'glob'; -import { extname, join, resolve } from 'path'; +import * as path from 'path'; import * as ts from 'typescript'; import { TSESTreeOptions } from '../../src/parser-options'; import { @@ -10,6 +10,7 @@ import { } from '../../tools/test-utils'; import { clearCaches, + createProgram, parseAndGenerateServices, ParseAndGenerateServicesResult, } from '../../src'; @@ -30,7 +31,7 @@ function createOptions(fileName: string): TSESTreeOptions & { cwd?: string } { useJSXTextNode: false, errorOnUnknownASTType: true, filePath: fileName, - tsconfigRootDir: join(process.cwd(), FIXTURES_DIR), + tsconfigRootDir: path.join(process.cwd(), FIXTURES_DIR), project: `./tsconfig.json`, loggerFn: false, }; @@ -42,9 +43,9 @@ beforeEach(() => clearCaches()); describe('semanticInfo', () => { // test all AST snapshots testFiles.forEach(filename => { - const code = readFileSync(join(FIXTURES_DIR, filename), 'utf8'); + const code = fs.readFileSync(path.join(FIXTURES_DIR, filename), 'utf8'); it( - formatSnapshotName(filename, FIXTURES_DIR, extname(filename)), + formatSnapshotName(filename, FIXTURES_DIR, path.extname(filename)), createSnapshotTestBlock( code, createOptions(filename), @@ -55,7 +56,7 @@ describe('semanticInfo', () => { it(`should cache the created ts.program`, () => { const filename = testFiles[0]; - const code = readFileSync(join(FIXTURES_DIR, filename), 'utf8'); + const code = fs.readFileSync(path.join(FIXTURES_DIR, filename), 'utf8'); const options = createOptions(filename); const optionsProjectString = { ...options, @@ -70,7 +71,7 @@ describe('semanticInfo', () => { it(`should handle "project": "./tsconfig.json" and "project": ["./tsconfig.json"] the same`, () => { const filename = testFiles[0]; - const code = readFileSync(join(FIXTURES_DIR, filename), 'utf8'); + const code = fs.readFileSync(path.join(FIXTURES_DIR, filename), 'utf8'); const options = createOptions(filename); const optionsProjectString = { ...options, @@ -87,7 +88,7 @@ describe('semanticInfo', () => { it(`should resolve absolute and relative tsconfig paths the same`, () => { const filename = testFiles[0]; - const code = readFileSync(join(FIXTURES_DIR, filename), 'utf8'); + const code = fs.readFileSync(path.join(FIXTURES_DIR, filename), 'utf8'); const options = createOptions(filename); const optionsAbsolutePath = { ...options, @@ -119,9 +120,9 @@ describe('semanticInfo', () => { // case-specific tests it('isolated-file tests', () => { - const fileName = resolve(FIXTURES_DIR, 'isolated-file.src.ts'); + const fileName = path.resolve(FIXTURES_DIR, 'isolated-file.src.ts'); const parseResult = parseCodeAndGenerateServices( - readFileSync(fileName, 'utf8'), + fs.readFileSync(fileName, 'utf8'), createOptions(fileName), ); @@ -129,9 +130,9 @@ describe('semanticInfo', () => { }); it('isolated-vue-file tests', () => { - const fileName = resolve(FIXTURES_DIR, 'extra-file-extension.vue'); + const fileName = path.resolve(FIXTURES_DIR, 'extra-file-extension.vue'); const parseResult = parseCodeAndGenerateServices( - readFileSync(fileName, 'utf8'), + fs.readFileSync(fileName, 'utf8'), { ...createOptions(fileName), extraFileExtensions: ['.vue'], @@ -142,9 +143,12 @@ describe('semanticInfo', () => { }); it('non-existent-estree-nodes tests', () => { - const fileName = resolve(FIXTURES_DIR, 'non-existent-estree-nodes.src.ts'); + const fileName = path.resolve( + FIXTURES_DIR, + 'non-existent-estree-nodes.src.ts', + ); const parseResult = parseCodeAndGenerateServices( - readFileSync(fileName, 'utf8'), + fs.readFileSync(fileName, 'utf8'), createOptions(fileName), ); @@ -166,9 +170,9 @@ describe('semanticInfo', () => { }); it('imported-file tests', () => { - const fileName = resolve(FIXTURES_DIR, 'import-file.src.ts'); + const fileName = path.resolve(FIXTURES_DIR, 'import-file.src.ts'); const parseResult = parseCodeAndGenerateServices( - readFileSync(fileName, 'utf8'), + fs.readFileSync(fileName, 'utf8'), createOptions(fileName), ); @@ -242,20 +246,26 @@ describe('semanticInfo', () => { }); it('non-existent project file', () => { - const fileName = resolve(FIXTURES_DIR, 'isolated-file.src.ts'); + const fileName = path.resolve(FIXTURES_DIR, 'isolated-file.src.ts'); const badConfig = createOptions(fileName); badConfig.project = './tsconfigs.json'; expect(() => - parseCodeAndGenerateServices(readFileSync(fileName, 'utf8'), badConfig), + parseCodeAndGenerateServices( + fs.readFileSync(fileName, 'utf8'), + badConfig, + ), ).toThrow(/Cannot read file .+tsconfigs\.json'/); }); it('fail to read project file', () => { - const fileName = resolve(FIXTURES_DIR, 'isolated-file.src.ts'); + const fileName = path.resolve(FIXTURES_DIR, 'isolated-file.src.ts'); const badConfig = createOptions(fileName); badConfig.project = '.'; expect(() => - parseCodeAndGenerateServices(readFileSync(fileName, 'utf8'), badConfig), + parseCodeAndGenerateServices( + fs.readFileSync(fileName, 'utf8'), + badConfig, + ), ).toThrow( // case insensitive because unix based systems are case insensitive /Cannot read file .+semanticInfo'./i, @@ -263,11 +273,14 @@ describe('semanticInfo', () => { }); it('malformed project file', () => { - const fileName = resolve(FIXTURES_DIR, 'isolated-file.src.ts'); + const fileName = path.resolve(FIXTURES_DIR, 'isolated-file.src.ts'); const badConfig = createOptions(fileName); badConfig.project = './badTSConfig/tsconfig.json'; expect(() => - parseCodeAndGenerateServices(readFileSync(fileName, 'utf8'), badConfig), + parseCodeAndGenerateServices( + fs.readFileSync(fileName, 'utf8'), + badConfig, + ), ).toThrowErrorMatchingSnapshot(); }); @@ -279,6 +292,45 @@ describe('semanticInfo', () => { expect(parseResult.services.program).toBeDefined(); }); + + it(`provided program instance is returned in result`, () => { + const filename = testFiles[0]; + const program = createProgram(path.join(FIXTURES_DIR, 'tsconfig.json')); + const code = fs.readFileSync(path.join(FIXTURES_DIR, filename), 'utf8'); + const options = createOptions(filename); + const optionsProjectString = { + ...options, + program: program, + project: './tsconfig.json', + }; + const parseResult = parseAndGenerateServices(code, optionsProjectString); + expect(parseResult.services.program).toBe(program); + }); + + it('file not in provided program instance', () => { + const filename = 'non-existant-file.ts'; + const program = createProgram(path.join(FIXTURES_DIR, 'tsconfig.json')); + const options = createOptions(filename); + const optionsProjectString = { + ...options, + program: program, + }; + expect(() => + parseAndGenerateServices('const foo = 5;', optionsProjectString), + ).toThrow( + `The file was not found in the provided program instance: ${filename}`, + ); + }); + + it('createProgram fails on non-existant file', () => { + expect(() => createProgram('tsconfig.non-existant.json')).toThrow(); + }); + + it('createProgram fails on tsconfig with errors', () => { + expect(() => + createProgram(path.join(FIXTURES_DIR, 'badTSConfig', 'tsconfig.json')), + ).toThrow(); + }); }); function testIsolatedFile(