Skip to content
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

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

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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
35 changes: 35 additions & 0 deletions packages/parser/README.md
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
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
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/parser/tests/tools/test-utils.ts
@@ -1,4 +1,7 @@
import { TSESTree } from '@typescript-eslint/typescript-estree';
import * as fs from 'fs';
uniqueiniquity marked this conversation as resolved.
Show resolved Hide resolved
import * as path from 'path';
import * as ts from 'typescript';
import * as parser from '../../src/parser';
import { ParserOptions } from '../../src/parser';

Expand Down
3 changes: 3 additions & 0 deletions packages/types/package.json
Expand Up @@ -48,5 +48,8 @@
"_ts3.4/*"
]
}
},
"devDependencies": {
"typescript": "*"
}
}
2 changes: 2 additions & 0 deletions 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')[];

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
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,
uniqueiniquity marked this conversation as resolved.
Show resolved Hide resolved
): 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
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
25 changes: 25 additions & 0 deletions 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 {
Expand Down Expand Up @@ -93,6 +94,29 @@ 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,
canonicalDirname,
Expand All @@ -101,4 +125,5 @@ export {
ensureAbsolutePath,
getCanonicalFileName,
getScriptKind,
getAstFromProgram,
};
@@ -0,0 +1,80 @@
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, 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.
*/
uniqueiniquity marked this conversation as resolved.
Show resolved Hide resolved
function createProgramFromConfigFile(
configFile: string,
projectDirectory: string = path.dirname(configFile),
): ts.Program {
const config = ts.readConfigFile(configFile, ts.sys.readFile);
if (config.error !== undefined) {
throw new Error(
ts.formatDiagnostics([config.error], {
getCanonicalFileName: f => f,
getCurrentDirectory: process.cwd,
getNewLine: () => '\n',
}),
);
}
const parseConfigHost: ts.ParseConfigHost = {
fileExists: fs.existsSync,
readDirectory: ts.sys.readDirectory,
readFile: file => fs.readFileSync(file, 'utf8'),
useCaseSensitiveFileNames: true,
};
const parsed = ts.parseJsonConfigFileContent(
config.config,
parseConfigHost,
path.resolve(projectDirectory),
{ noEmit: true },
uniqueiniquity marked this conversation as resolved.
Show resolved Hide resolved
);
if (parsed.errors.length) {
throw new Error(
ts.formatDiagnostics(parsed.errors, {
getCanonicalFileName: f => f,
getCurrentDirectory: process.cwd,
getNewLine: () => '\n',
}),
);
}
const host = ts.createCompilerHost(parsed.options, true);
const program = ts.createProgram(parsed.fileNames, parsed.options, host);

return program;
}

export { useProvidedProgram, createProgramFromConfigFile };
1 change: 1 addition & 0 deletions packages/typescript-estree/src/index.ts
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
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