diff --git a/.prettierignore b/.prettierignore index 1dd0c3996671..a86a2f04fc90 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,4 +1,5 @@ **/tests/fixtures/**/* +**/tests/fixture-project/**/* **/dist **/coverage **/shared-fixtures diff --git a/packages/eslint-plugin-tslint/tests/fixture-project/1.ts b/packages/eslint-plugin-tslint/tests/fixture-project/1.ts new file mode 100644 index 000000000000..0870b5cd2825 --- /dev/null +++ b/packages/eslint-plugin-tslint/tests/fixture-project/1.ts @@ -0,0 +1 @@ +var foo = true; diff --git a/packages/eslint-plugin-tslint/tests/fixture-project/2.ts b/packages/eslint-plugin-tslint/tests/fixture-project/2.ts new file mode 100644 index 000000000000..3da535b2cde3 --- /dev/null +++ b/packages/eslint-plugin-tslint/tests/fixture-project/2.ts @@ -0,0 +1 @@ +throw 'should be ok because rule is not loaded'; diff --git a/packages/eslint-plugin-tslint/tests/fixture-project/3.ts b/packages/eslint-plugin-tslint/tests/fixture-project/3.ts new file mode 100644 index 000000000000..e71f830c3915 --- /dev/null +++ b/packages/eslint-plugin-tslint/tests/fixture-project/3.ts @@ -0,0 +1 @@ +throw 'err'; // no-string-throw diff --git a/packages/eslint-plugin-tslint/tests/fixture-project/4.ts b/packages/eslint-plugin-tslint/tests/fixture-project/4.ts new file mode 100644 index 000000000000..1ca8bbace361 --- /dev/null +++ b/packages/eslint-plugin-tslint/tests/fixture-project/4.ts @@ -0,0 +1 @@ +var foo = true // semicolon diff --git a/packages/eslint-plugin-tslint/tests/fixture-project/5.ts b/packages/eslint-plugin-tslint/tests/fixture-project/5.ts new file mode 100644 index 000000000000..2fc07810720c --- /dev/null +++ b/packages/eslint-plugin-tslint/tests/fixture-project/5.ts @@ -0,0 +1 @@ +var foo = true; // fail diff --git a/packages/eslint-plugin-tslint/tests/fixture-project/6.ts b/packages/eslint-plugin-tslint/tests/fixture-project/6.ts new file mode 100644 index 000000000000..e901f01b4874 --- /dev/null +++ b/packages/eslint-plugin-tslint/tests/fixture-project/6.ts @@ -0,0 +1 @@ +foo; diff --git a/packages/eslint-plugin-tslint/tests/fixture-project/tsconfig.json b/packages/eslint-plugin-tslint/tests/fixture-project/tsconfig.json new file mode 100644 index 000000000000..0967ef424bce --- /dev/null +++ b/packages/eslint-plugin-tslint/tests/fixture-project/tsconfig.json @@ -0,0 +1 @@ +{} diff --git a/packages/eslint-plugin-tslint/tests/index.spec.ts b/packages/eslint-plugin-tslint/tests/index.spec.ts index 88a3a648b076..0de1046ecf3f 100644 --- a/packages/eslint-plugin-tslint/tests/index.spec.ts +++ b/packages/eslint-plugin-tslint/tests/index.spec.ts @@ -12,7 +12,7 @@ const ruleTester = new TSESLint.RuleTester({ * Project is needed to generate the parserServices * within @typescript-eslint/parser */ - project: './tests/tsconfig.json', + project: './tests/fixture-project/tsconfig.json', }, parser: require.resolve('@typescript-eslint/parser'), }); @@ -47,6 +47,7 @@ ruleTester.run('tslint/config', rule, { { code: 'var foo = true;', options: tslintRulesConfig, + filename: './tests/fixture-project/1.ts', }, { filename: './tests/test-project/file-spec.ts', @@ -62,6 +63,7 @@ ruleTester.run('tslint/config', rule, { { code: 'throw "should be ok because rule is not loaded";', options: tslintRulesConfig, + filename: './tests/fixture-project/2.ts', }, ], @@ -69,6 +71,7 @@ ruleTester.run('tslint/config', rule, { { options: [{ lintFile: './tests/test-project/tslint.json' }], code: 'throw "err" // no-string-throw', + filename: './tests/fixture-project/3.ts', errors: [ { messageId: 'failure', @@ -84,6 +87,7 @@ ruleTester.run('tslint/config', rule, { code: 'var foo = true // semicolon', options: tslintRulesConfig, output: 'var foo = true // semicolon', + filename: './tests/fixture-project/4.ts', errors: [ { messageId: 'failure', @@ -100,6 +104,7 @@ ruleTester.run('tslint/config', rule, { code: 'var foo = true // fail', options: tslintRulesDirectoryConfig, output: 'var foo = true // fail', + filename: './tests/fixture-project/5.ts', errors: [ { messageId: 'failure', @@ -174,26 +179,30 @@ describe('tslint/error', () => { }); }); - it('should not crash if there is no tslint rules specified', () => { + it('should not crash if there are no tslint rules specified', () => { const linter = new TSESLint.Linter(); jest.spyOn(console, 'warn').mockImplementation(); linter.defineRule('tslint/config', rule); linter.defineParser('@typescript-eslint/parser', parser); expect(() => - linter.verify('foo;', { - parserOptions: { - project: `${__dirname}/test-project/tsconfig.json`, - }, - rules: { - 'tslint/config': [2, {}], + linter.verify( + 'foo;', + { + parserOptions: { + project: `${__dirname}/test-project/tsconfig.json`, + }, + rules: { + 'tslint/config': [2, {}], + }, + parser: '@typescript-eslint/parser', }, - parser: '@typescript-eslint/parser', - }), + `${__dirname}/test-project/extra.ts`, + ), ).not.toThrow(); expect(console.warn).toHaveBeenCalledWith( expect.stringContaining( - 'Tried to lint but found no valid, enabled rules for this file type and file path in the resolved configuration.', + `Tried to lint ${__dirname}/test-project/extra.ts but found no valid, enabled rules for this file type and file path in the resolved configuration.`, ), ); jest.resetAllMocks(); diff --git a/packages/eslint-plugin-tslint/tests/test-project/extra.ts b/packages/eslint-plugin-tslint/tests/test-project/extra.ts new file mode 100644 index 000000000000..e901f01b4874 --- /dev/null +++ b/packages/eslint-plugin-tslint/tests/test-project/extra.ts @@ -0,0 +1 @@ +foo; diff --git a/packages/eslint-plugin/tests/RuleTester.ts b/packages/eslint-plugin/tests/RuleTester.ts index 0733e1887cb2..fe3d06d4fc87 100644 --- a/packages/eslint-plugin/tests/RuleTester.ts +++ b/packages/eslint-plugin/tests/RuleTester.ts @@ -7,6 +7,8 @@ type RuleTesterConfig = Omit & { parser: typeof parser; }; class RuleTester extends TSESLint.RuleTester { + private filename: string | undefined = undefined; + // as of eslint 6 you have to provide an absolute path to the parser // but that's not as clean to type, this saves us trying to manually enforce // that contributors require.resolve everything @@ -15,6 +17,10 @@ class RuleTester extends TSESLint.RuleTester { ...options, parser: require.resolve(options.parser), }); + + if (options.parserOptions && options.parserOptions.project) { + this.filename = path.join(getFixturesRootDir(), 'file.ts'); + } } // as of eslint 6 you have to provide an absolute path to the parser @@ -26,17 +32,36 @@ class RuleTester extends TSESLint.RuleTester { tests: TSESLint.RunTests, ): void { const errorMessage = `Do not set the parser at the test level unless you want to use a parser other than ${parser}`; + + if (this.filename) { + tests.valid = tests.valid.map(test => { + if (typeof test === 'string') { + return { + code: test, + filename: this.filename, + }; + } + return test; + }); + } + tests.valid.forEach(test => { if (typeof test !== 'string') { if (test.parser === parser) { throw new Error(errorMessage); } + if (!test.filename) { + test.filename = this.filename; + } } }); tests.invalid.forEach(test => { if (test.parser === parser) { throw new Error(errorMessage); } + if (!test.filename) { + test.filename = this.filename; + } }); super.run(name, rule, tests); diff --git a/packages/eslint-plugin/tests/fixtures/file.ts b/packages/eslint-plugin/tests/fixtures/file.ts new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/eslint-plugin/tests/rules/no-unnecessary-qualifier.test.ts b/packages/eslint-plugin/tests/rules/no-unnecessary-qualifier.test.ts index 53a349e552ee..75236704c853 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-qualifier.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-qualifier.test.ts @@ -199,7 +199,6 @@ import * as Foo from './foo'; declare module './foo' { const x: Foo.T = 3; }`, - filename: path.join(rootPath, 'bar.ts'), errors: [ { messageId, diff --git a/packages/parser/README.md b/packages/parser/README.md index 78d17e6388fb..45c620280f15 100644 --- a/packages/parser/README.md +++ b/packages/parser/README.md @@ -50,8 +50,23 @@ The following additional configuration options are available by specifying them - **`project`** - default `undefined`. This option allows you to provide a path to your project's `tsconfig.json`. **This setting is required if you want to use rules which require type information**. You may want to use this setting in tandem with the `tsconfigRootDir` option below. + - Note that if this setting is specified and `createDefaultProgram` is not, you must only lint files that are included in the projects as defined by the provided `tsconfig.json` files. If your existing configuration does not include all of the files you would like to lint, you can create a separate `tsconfig.eslint.json` as follows: + + ```ts + { + "extends": "./tsconfig.json", // path to existing tsconfig + "includes": [ + "src/**/*.ts", + "test/**/*.ts", + // etc + ] + } + ``` + - **`tsconfigRootDir`** - default `undefined`. This option allows you to provide the root directory for relative tsconfig paths specified in the `project` option above. +- **`createDefaultProgram`** - 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. However, this may incur significant performance costs, so this option is primarily included for backwards-compatibility. See the **`project`** section for more information. + - **`extraFileExtensions`** - default `undefined`. This option allows you to provide one or more additional file extensions which should be considered in the TypeScript Program compilation. E.g. a `.vue` file - **`warnOnUnsupportedTypeScriptVersion`** - default `true`. This option allows you to toggle the warning that the parser will give you if you use a version of TypeScript which is not explicitly supported diff --git a/packages/parser/tests/lib/parser.ts b/packages/parser/tests/lib/parser.ts index 0a00ef5ad155..782d474afc36 100644 --- a/packages/parser/tests/lib/parser.ts +++ b/packages/parser/tests/lib/parser.ts @@ -50,12 +50,12 @@ describe('parser', () => { jsx: false, }, // ts-estree specific - filePath: 'test/foo', + filePath: 'tests/fixtures/services/isolated-file.src.ts', project: 'tsconfig.json', useJSXTextNode: false, errorOnUnknownASTType: false, errorOnTypeScriptSyntacticAndSemanticIssues: false, - tsconfigRootDir: './', + tsconfigRootDir: 'tests/fixtures/services', extraFileExtensions: ['foo'], }; parseForESLint(code, config); diff --git a/packages/typescript-estree/src/parser-options.ts b/packages/typescript-estree/src/parser-options.ts index c224b7da5659..77a649f6f309 100644 --- a/packages/typescript-estree/src/parser-options.ts +++ b/packages/typescript-estree/src/parser-options.ts @@ -18,6 +18,7 @@ export interface Extra { tsconfigRootDir: string; extraFileExtensions: string[]; preserveNodeMaps?: boolean; + createDefaultProgram: boolean; } export interface TSESTreeOptions { @@ -35,6 +36,7 @@ export interface TSESTreeOptions { tsconfigRootDir?: string; extraFileExtensions?: string[]; preserveNodeMaps?: boolean; + createDefaultProgram?: boolean; } // This lets us use generics to type the return value, and removes the need to diff --git a/packages/typescript-estree/src/parser.ts b/packages/typescript-estree/src/parser.ts index aed8f04b8d27..564114d7ccb7 100644 --- a/packages/typescript-estree/src/parser.ts +++ b/packages/typescript-estree/src/parser.ts @@ -58,6 +58,7 @@ function resetExtra(): void { tsconfigRootDir: process.cwd(), extraFileExtensions: [], preserveNodeMaps: undefined, + createDefaultProgram: false, }; } @@ -66,20 +67,27 @@ function resetExtra(): void { * @param options The config object * @returns If found, returns the source file corresponding to the code and the containing program */ -function getASTFromProject(code: string, options: TSESTreeOptions) { - return firstDefined( - calculateProjectParserOptions( - code, - options.filePath || getFileName(options), - extra, - ), +function getASTFromProject( + code: string, + options: TSESTreeOptions, + createDefaultProgram: boolean, +) { + const filePath = options.filePath || getFileName(options); + const astAndProgram = firstDefined( + calculateProjectParserOptions(code, filePath, extra), currentProgram => { - const ast = currentProgram.getSourceFile( - options.filePath || getFileName(options), - ); + const ast = currentProgram.getSourceFile(filePath); return ast && { ast, program: currentProgram }; }, ); + + if (!astAndProgram && !createDefaultProgram) { + throw new Error( + `If "parserOptions.project" has been set for @typescript-eslint/parser, ${filePath} must be included in at least one of the projects provided.`, + ); + } + + return astAndProgram; } /** @@ -161,10 +169,14 @@ function getProgramAndAST( code: string, options: TSESTreeOptions, shouldProvideParserServices: boolean, + createDefaultProgram: boolean, ) { return ( - (shouldProvideParserServices && getASTFromProject(code, options)) || - (shouldProvideParserServices && getASTAndDefaultProject(code, options)) || + (shouldProvideParserServices && + getASTFromProject(code, options, createDefaultProgram)) || + (shouldProvideParserServices && + createDefaultProgram && + getASTAndDefaultProject(code, options)) || createNewProgram(code) ); } @@ -254,6 +266,10 @@ function applyParserOptionsToExtra(options: TSESTreeOptions): void { if (options.preserveNodeMaps === undefined && extra.projects.length > 0) { extra.preserveNodeMaps = true; } + + extra.createDefaultProgram = + typeof options.createDefaultProgram === 'boolean' && + options.createDefaultProgram; } function warnAboutTSVersion(): void { @@ -386,6 +402,7 @@ export function parseAndGenerateServices< code, options, shouldProvideParserServices, + extra.createDefaultProgram, ); /** * Determine whether or not two-way maps of converted AST nodes should be preserved diff --git a/packages/typescript-estree/src/tsconfig-parser.ts b/packages/typescript-estree/src/tsconfig-parser.ts index 855355fe689b..091990533498 100644 --- a/packages/typescript-estree/src/tsconfig-parser.ts +++ b/packages/typescript-estree/src/tsconfig-parser.ts @@ -30,6 +30,16 @@ const watchCallbackTrackingMap = new Map(); const parsedFilesSeen = new Set(); +/** + * Clear tsconfig caches. + * Primarily used for testing. + */ +export function clearCaches() { + knownWatchProgramMap.clear(); + watchCallbackTrackingMap.clear(); + parsedFilesSeen.clear(); +} + /** * Holds information about the file currently being linted */ diff --git a/packages/typescript-estree/tests/fixtures/simpleProject/file.ts b/packages/typescript-estree/tests/fixtures/simpleProject/file.ts new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/typescript-estree/tests/fixtures/simpleProject/tsconfig.json b/packages/typescript-estree/tests/fixtures/simpleProject/tsconfig.json new file mode 100644 index 000000000000..0967ef424bce --- /dev/null +++ b/packages/typescript-estree/tests/fixtures/simpleProject/tsconfig.json @@ -0,0 +1 @@ +{} diff --git a/packages/typescript-estree/tests/lib/parse.ts b/packages/typescript-estree/tests/lib/parse.ts index 3d8dfe222e45..57a4bc057424 100644 --- a/packages/typescript-estree/tests/lib/parse.ts +++ b/packages/typescript-estree/tests/lib/parse.ts @@ -2,6 +2,9 @@ import * as parser from '../../src/parser'; import * as astConverter from '../../src/ast-converter'; import { TSESTreeOptions } from '../../src/parser-options'; import { createSnapshotTestBlock } from '../../tools/test-utils'; +import { join } from 'path'; + +const FIXTURES_DIR = './tests/fixtures/simpleProject'; describe('parse()', () => { describe('basic functionality', () => { @@ -91,6 +94,7 @@ describe('parse()', () => { tsconfigRootDir: expect.any(String), useJSXTextNode: false, preserveNodeMaps: false, + createDefaultProgram: false, }, false, ); @@ -137,6 +141,12 @@ describe('parse()', () => { tokens: true, range: true, loc: true, + filePath: 'tests/fixtures/simpleProject/file.ts', + }; + const projectConfig: TSESTreeOptions = { + ...baseConfig, + tsconfigRootDir: join(process.cwd(), FIXTURES_DIR), + project: './tsconfig.json', }; it('should not impact the use of parse()', () => { @@ -167,10 +177,10 @@ describe('parse()', () => { expect(noOptionSet.services.esTreeNodeToTSNodeMap).toBeUndefined(); expect(noOptionSet.services.tsNodeToESTreeNodeMap).toBeUndefined(); - const withProjectNoOptionSet = parser.parseAndGenerateServices(code, { - ...baseConfig, - project: './tsconfig.json', - }); + const withProjectNoOptionSet = parser.parseAndGenerateServices( + code, + projectConfig, + ); expect(withProjectNoOptionSet.services.esTreeNodeToTSNodeMap).toEqual( expect.any(WeakMap), @@ -194,9 +204,8 @@ describe('parse()', () => { ); const withProjectOptionSetToTrue = parser.parseAndGenerateServices(code, { - ...baseConfig, + ...projectConfig, preserveNodeMaps: true, - project: './tsconfig.json', }); expect(withProjectOptionSetToTrue.services.esTreeNodeToTSNodeMap).toEqual( @@ -218,7 +227,10 @@ describe('parse()', () => { const withProjectOptionSetToFalse = parser.parseAndGenerateServices( code, - { ...baseConfig, preserveNodeMaps: false, project: './tsconfig.json' }, + { + ...projectConfig, + preserveNodeMaps: false, + }, ); expect( diff --git a/packages/typescript-estree/tests/lib/semanticInfo.ts b/packages/typescript-estree/tests/lib/semanticInfo.ts index 3f0a69b30537..7e5c634db9d3 100644 --- a/packages/typescript-estree/tests/lib/semanticInfo.ts +++ b/packages/typescript-estree/tests/lib/semanticInfo.ts @@ -13,6 +13,7 @@ import { ParseAndGenerateServicesResult, } from '../../src/parser'; import { TSESTree } from '../../src/ts-estree'; +import { clearCaches } from '../../src/tsconfig-parser'; const FIXTURES_DIR = './tests/fixtures/semanticInfo'; const testFiles = glob.sync(`${FIXTURES_DIR}/**/*.src.ts`); @@ -28,11 +29,14 @@ function createOptions(fileName: string): TSESTreeOptions & { cwd?: string } { errorOnUnknownASTType: true, filePath: fileName, tsconfigRootDir: join(process.cwd(), FIXTURES_DIR), - project: './tsconfig.json', + project: `./tsconfig.json`, loggerFn: false, }; } +// ensure tsconfig-parser caches are clean for each test +beforeEach(() => clearCaches()); + describe('semanticInfo', () => { // test all AST snapshots testFiles.forEach(filename => { @@ -193,12 +197,14 @@ describe('semanticInfo', () => { it('non-existent file tests', () => { const parseResult = parseCodeAndGenerateServices( `const x = [parseInt("5")];`, - createOptions(''), + { + ...createOptions(''), + project: undefined, + preserveNodeMaps: true, + }, ); - // get type checker - expect(parseResult).toHaveProperty('services.program.getTypeChecker'); - const checker = parseResult.services.program!.getTypeChecker(); + expect(parseResult.services.program).toBeUndefined(); // get bound name const boundName = (parseResult.ast.body[0] as TSESTree.VariableDeclaration) @@ -210,8 +216,6 @@ describe('semanticInfo', () => { ); expect(tsBoundName).toBeDefined(); - checkNumberArrayType(checker, tsBoundName); - expect(parseResult.services.tsNodeToESTreeNodeMap!.get(tsBoundName)).toBe( boundName, ); @@ -220,18 +224,21 @@ describe('semanticInfo', () => { it('non-existent file should provide parents nodes', () => { const parseResult = parseCodeAndGenerateServices( `function M() { return Base }`, - createOptions(''), + { ...createOptions(''), project: undefined }, ); - // https://github.com/JamesHenry/typescript-estree/issues/77 - expect(parseResult.services.program).toBeDefined(); - expect( - parseResult.services.program!.getSourceFile(''), - ).toBeDefined(); - expect( - parseResult.services.program!.getSourceFile('')!.statements[0] - .parent, - ).toBeDefined(); + expect(parseResult.services.program).toBeUndefined(); + }); + + it(`non-existent file should throw error when project provided`, () => { + expect(() => + parseCodeAndGenerateServices( + `function M() { return Base }`, + createOptions(''), + ), + ).toThrow( + `If "parserOptions.project" has been set for @typescript-eslint/parser, must be included in at least one of the projects provided.`, + ); }); it('non-existent project file', () => { @@ -260,6 +267,15 @@ describe('semanticInfo', () => { parseCodeAndGenerateServices(readFileSync(fileName, 'utf8'), badConfig), ).toThrowErrorMatchingSnapshot(); }); + + it('default program produced with option', () => { + const parseResult = parseCodeAndGenerateServices('var foo = 5;', { + ...createOptions(''), + createDefaultProgram: true, + }); + + expect(parseResult.services.program).toBeDefined(); + }); }); function testIsolatedFile( diff --git a/tests/integration/utils/.eslintrc.js b/tests/integration/utils/.eslintrc.js new file mode 100644 index 000000000000..8ca32766a124 --- /dev/null +++ b/tests/integration/utils/.eslintrc.js @@ -0,0 +1,5 @@ +module.exports = { + parserOptions: { + project: `${__dirname}/jsconfig.json`, + }, +}; diff --git a/tests/integration/utils/jsconfig.json b/tests/integration/utils/jsconfig.json new file mode 100644 index 000000000000..d53d21eadfbb --- /dev/null +++ b/tests/integration/utils/jsconfig.json @@ -0,0 +1,3 @@ +{ + "exclude": [".eslintrc.js"] +} pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy