Skip to content

feat: allow user to provide TS program instance in parser options #3484

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
Jun 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions packages/parser/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ interface ParserOptions {
tsconfigRootDir?: string;
extraFileExtensions?: string[];
warnOnUnsupportedTypeScriptVersion?: boolean;

program?: import('typescript').Program;
}
```

Expand Down Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions packages/parser/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 8 additions & 5 deletions packages/parser/tests/lib/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
formatSnapshotName,
testServices,
} from '../tools/test-utils';
import { createProgram } from '@typescript-eslint/typescript-estree';

//------------------------------------------------------------------------------
// Setup
Expand All @@ -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 });
});
});
});
3 changes: 3 additions & 0 deletions packages/types/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,8 @@
"_ts3.4/*"
]
}
},
"devDependencies": {
"typescript": "*"
}
}
2 changes: 2 additions & 0 deletions packages/types/src/parser-options.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Lib } from './lib';
import type { Program } from 'typescript';

type DebugLevel = boolean | ('typescript-eslint' | 'eslint' | 'typescript')[];

Expand Down Expand Up @@ -41,6 +42,7 @@ interface ParserOptions {
extraFileExtensions?: string[];
filePath?: string;
loc?: boolean;
program?: Program;
project?: string | string[];
projectFolderIgnoreList?: (string | RegExp)[];
range?: boolean;
Expand Down
35 changes: 35 additions & 0 deletions packages/typescript-estree/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. *
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand Down
47 changes: 40 additions & 7 deletions packages/typescript-estree/src/create-program/shared.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import path from 'path';
import * as ts from 'typescript';
import { Program } from 'typescript';
import { Extra } from '../parser-options';

interface ASTAndProgram {
Expand All @@ -8,21 +9,28 @@ 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
*/
noUnusedLocals: true,
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 {
Expand Down Expand Up @@ -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,
};
Original file line number Diff line number Diff line change
@@ -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 };
1 change: 1 addition & 0 deletions packages/typescript-estree/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
8 changes: 8 additions & 0 deletions packages/typescript-estree/src/parser-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export interface Extra {
loc: boolean;
log: (message: string) => void;
preserveNodeMaps?: boolean;
program: null | Program;
projects: CanonicalPath[];
range: boolean;
strict: boolean;
Expand Down Expand Up @@ -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. *
Expand Down
Loading