Skip to content

Commit

Permalink
fix(typescript-estree): allow providing more one than one existing pr…
Browse files Browse the repository at this point in the history
…ogram in config (#3508)
  • Loading branch information
JamesHenry committed Jun 9, 2021
1 parent ced9b26 commit 4f1806e
Show file tree
Hide file tree
Showing 10 changed files with 86 additions and 49 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.js
Expand Up @@ -27,7 +27,7 @@ module.exports = {
],
tsconfigRootDir: __dirname,
warnOnUnsupportedTypeScriptVersion: false,
EXPERIMENTAL_useSourceOfProjectReferenceRedirect: true,
EXPERIMENTAL_useSourceOfProjectReferenceRedirect: false,
},
rules: {
//
Expand Down
Expand Up @@ -3,12 +3,10 @@ import * as eslintUtils from 'eslint-utils';
import { TSESTree } from '../../ts-estree';
import * as TSESLint from '../../ts-eslint';

/* eslint-disable @typescript-eslint/no-unsafe-assignment */
const ReferenceTrackerREAD: unique symbol = eslintUtils.ReferenceTracker.READ;
const ReferenceTrackerCALL: unique symbol = eslintUtils.ReferenceTracker.CALL;
const ReferenceTrackerCONSTRUCT: unique symbol =
eslintUtils.ReferenceTracker.CONSTRUCT;
/* eslint-enable @typescript-eslint/no-unsafe-assignment */

interface ReferenceTracker {
/**
Expand Down
1 change: 0 additions & 1 deletion packages/experimental-utils/src/ts-eslint-scope/index.ts
Expand Up @@ -10,5 +10,4 @@ export * from './Scope';
export * from './ScopeManager';
export * from './Variable';

// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
export const version: string = ESLintVersion;
14 changes: 7 additions & 7 deletions packages/parser/README.md
Expand Up @@ -213,19 +213,19 @@ 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`
### `parserOptions.programs`

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.
This option allows you to programmatically provide an array of one or more instances of a TypeScript Program object that will provide type information to rules.
This will override any programs that would have been computed from `parserOptions.project` or `parserOptions.createDefaultProgram`.
All linted files must be part of the provided program(s).

## 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.
This serves as a utility method for users of the `parserOptions.programs` feature to create a TypeScript program instance from a config file.

```ts
declare function createProgram(
Expand All @@ -238,10 +238,10 @@ Example usage in .eslintrc.js:

```js
const parser = require('@typescript-eslint/parser');
const program = parser.createProgram('tsconfig.json');
const programs = [parser.createProgram('tsconfig.json')];
module.exports = {
parserOptions: {
program,
programs,
},
};
```
Expand Down
Expand Up @@ -11,29 +11,39 @@ import {

const log = debug('typescript-eslint:typescript-estree:useProvidedProgram');

function useProvidedProgram(
programInstance: ts.Program,
function useProvidedPrograms(
programInstances: 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
log(
'Retrieving ast for %s from provided program instance(s)',
extra.filePath,
);

const astAndProgram = getAstFromProgram(programInstance, extra);
let astAndProgram: ASTAndProgram | undefined;
for (const programInstance of programInstances) {
astAndProgram = getAstFromProgram(programInstance, extra);
// Stop at the first applicable program instance
if (astAndProgram) {
break;
}
}

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}`,
'"parserOptions.programs" has been provided for @typescript-eslint/parser.',
`The file was not found in any of the provided program instance(s): ${relativeFilePath}`,
];

throw new Error(errorLines.join('\n'));
}

astAndProgram.program.getTypeChecker(); // ensure parent pointers are set in source files

return astAndProgram;
}

Expand Down Expand Up @@ -84,4 +94,4 @@ function formatDiagnostics(diagnostics: ts.Diagnostic[]): string | undefined {
});
}

export { useProvidedProgram, createProgramFromConfigFile };
export { useProvidedPrograms, createProgramFromConfigFile };
2 changes: 1 addition & 1 deletion packages/typescript-estree/src/index.ts
Expand Up @@ -3,7 +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';
export { createProgramFromConfigFile as createProgram } from './create-program/useProvidedPrograms';

// re-export for backwards-compat
export { visitorKeys } from '@typescript-eslint/visitor-keys';
Expand Down
8 changes: 4 additions & 4 deletions packages/typescript-estree/src/parser-options.ts
Expand Up @@ -20,7 +20,7 @@ export interface Extra {
loc: boolean;
log: (message: string) => void;
preserveNodeMaps?: boolean;
program: null | Program;
programs: null | Program[];
projects: CanonicalPath[];
range: boolean;
strict: boolean;
Expand Down Expand Up @@ -171,11 +171,11 @@ interface ParseAndGenerateServicesOptions extends ParseOptions {
tsconfigRootDir?: string;

/**
* Instance of a TypeScript Program object to be used for type information.
* An array of one or more instances of TypeScript Program objects 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.
* All linted files must be part of the provided program(s).
*/
program?: Program;
programs?: Program[];

/**
***************************************************************************************
Expand Down
27 changes: 17 additions & 10 deletions packages/typescript-estree/src/parser.ts
Expand Up @@ -19,7 +19,7 @@ import {
getCanonicalFileName,
} from './create-program/shared';
import { Program } from 'typescript';
import { useProvidedProgram } from './create-program/useProvidedProgram';
import { useProvidedPrograms } from './create-program/useProvidedPrograms';

const log = debug('typescript-eslint:typescript-estree:parser');

Expand Down Expand Up @@ -57,18 +57,20 @@ function enforceString(code: unknown): string {

/**
* @param code The code of the file being linted
* @param programInstances One or more existing programs to use
* @param shouldProvideParserServices True if the program should be attempted to be calculated from provided tsconfig files
* @param shouldCreateDefaultProgram True if the program should be created from compiler host
* @returns Returns a source file and program corresponding to the linted code
*/
function getProgramAndAST(
code: string,
programInstance: Program | null,
programInstances: Program[] | null,
shouldProvideParserServices: boolean,
shouldCreateDefaultProgram: boolean,
): ASTAndProgram {
return (
(programInstance && useProvidedProgram(programInstance, extra)) ||
(programInstances?.length &&
useProvidedPrograms(programInstances, extra)) ||
(shouldProvideParserServices &&
createProjectProgram(code, shouldCreateDefaultProgram, extra)) ||
(shouldProvideParserServices &&
Expand Down Expand Up @@ -109,7 +111,7 @@ function resetExtra(): void {
loc: false,
log: console.log, // eslint-disable-line no-console
preserveNodeMaps: true,
program: null,
programs: null,
projects: [],
range: false,
strict: false,
Expand Down Expand Up @@ -269,14 +271,19 @@ function applyParserOptionsToExtra(options: TSESTreeOptions): void {
// NOTE - ensureAbsolutePath relies upon having the correct tsconfigRootDir in extra
extra.filePath = ensureAbsolutePath(extra.filePath, extra);

if (options.program && typeof options.program === 'object') {
extra.program = options.program;
if (Array.isArray(options.programs)) {
if (!options.programs.length) {
throw new Error(
`You have set parserOptions.programs to an empty array. This will cause all files to not be found in existing programs. Either provide one or more existing TypeScript Program instances in the array, or remove the parserOptions.programs setting.`,
);
}
extra.programs = options.programs;
log(
'parserOptions.program was provided, so parserOptions.project will be ignored.',
'parserOptions.programs was provided, so parserOptions.project will be ignored.',
);
}

if (!extra.program) {
if (!extra.programs) {
// providing a program overrides project resolution
const projectFolderIgnoreList = (
options.projectFolderIgnoreList ?? ['**/node_modules/**']
Expand Down Expand Up @@ -464,10 +471,10 @@ function parseAndGenerateServices<T extends TSESTreeOptions = TSESTreeOptions>(
* 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.program != null || (extra.projects && extra.projects.length > 0);
extra.programs != null || (extra.projects && extra.projects.length > 0);
const { ast, program } = getProgramAndAST(
code,
extra.program,
extra.programs,
shouldProvideParserServices,
extra.createDefaultProgram,
)!;
Expand Down
47 changes: 34 additions & 13 deletions packages/typescript-estree/tests/lib/semanticInfo.test.ts
Expand Up @@ -293,37 +293,58 @@ describe('semanticInfo', () => {
expect(parseResult.services.program).toBeDefined();
});

it(`provided program instance is returned in result`, () => {
it('empty programs array should throw', () => {
const fileName = path.resolve(FIXTURES_DIR, 'isolated-file.src.ts');
const badConfig = createOptions(fileName);
badConfig.programs = [];
expect(() => parseAndGenerateServices('const foo = 5;', badConfig)).toThrow(
'You have set parserOptions.programs to an empty array. This will cause all files to not be found in existing programs. Either provide one or more existing TypeScript Program instances in the array, or remove the parserOptions.programs setting.',
);
});

it(`first matching provided program instance is returned in result`, () => {
const filename = testFiles[0];
const program = createProgram(path.join(FIXTURES_DIR, 'tsconfig.json'));
const program1 = createProgram(path.join(FIXTURES_DIR, 'tsconfig.json'));
const program2 = 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,
programs: [program1, program2],
project: './tsconfig.json',
};
const parseResult = parseAndGenerateServices(code, optionsProjectString);
expect(parseResult.services.program).toBe(program);
expect(parseResult.services.program).toBe(program1);
});

it('file not in provided program instance', () => {
const filename = 'non-existant-file.ts';
const program = createProgram(path.join(FIXTURES_DIR, 'tsconfig.json'));
it('file not in provided program instance(s)', () => {
const filename = 'non-existent-file.ts';
const program1 = createProgram(path.join(FIXTURES_DIR, 'tsconfig.json'));
const options = createOptions(filename);
const optionsProjectString = {
const optionsWithSingleProgram = {
...options,
programs: [program1],
};
expect(() =>
parseAndGenerateServices('const foo = 5;', optionsWithSingleProgram),
).toThrow(
`The file was not found in any of the provided program instance(s): ${filename}`,
);

const program2 = createProgram(path.join(FIXTURES_DIR, 'tsconfig.json'));
const optionsWithMultiplePrograms = {
...options,
program: program,
programs: [program1, program2],
};
expect(() =>
parseAndGenerateServices('const foo = 5;', optionsProjectString),
parseAndGenerateServices('const foo = 5;', optionsWithMultiplePrograms),
).toThrow(
`The file was not found in the provided program instance: ${filename}`,
`The file was not found in any of the provided program instance(s): ${filename}`,
);
});

it('createProgram fails on non-existant file', () => {
expect(() => createProgram('tsconfig.non-existant.json')).toThrow();
it('createProgram fails on non-existent file', () => {
expect(() => createProgram('tsconfig.non-existent.json')).toThrow();
});

it('createProgram fails on tsconfig with errors', () => {
Expand Down
4 changes: 3 additions & 1 deletion tsconfig.eslint.json
@@ -1,6 +1,8 @@
{
"compilerOptions": {
"types": ["@types/node"]
"types": ["@types/node"],
"noEmit": true,
"allowJs": true
},
"extends": "./tsconfig.base.json",
"include": ["tests/**/*.ts", "tools/**/*.ts", ".eslintrc.js"]
Expand Down

0 comments on commit 4f1806e

Please sign in to comment.