From 1e29e69b289d61107a7de67592beae331ba50222 Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Fri, 27 Mar 2020 09:31:51 -0700 Subject: [PATCH] feat(typescript-estree): add option to ignore certain folders from glob resolution (#1802) --- .../src/ts-eslint/ParserOptions.ts | 1 + packages/parser/README.md | 19 +++- packages/typescript-estree/README.md | 11 ++ .../typescript-estree/src/parser-options.ts | 91 +++++++++------- packages/typescript-estree/src/parser.ts | 100 ++++++++++++++---- .../projectFolderIgnoreList/ignoreme/file.ts | 1 + .../ignoreme/tsconfig.json | 3 + .../projectFolderIgnoreList/includeme/file.ts | 1 + .../includeme/tsconfig.json | 3 + packages/typescript-estree/tests/lib/parse.ts | 45 ++++++++ tsconfig.eslint.json | 2 +- 11 files changed, 213 insertions(+), 64 deletions(-) create mode 100644 packages/typescript-estree/tests/fixtures/projectFolderIgnoreList/ignoreme/file.ts create mode 100644 packages/typescript-estree/tests/fixtures/projectFolderIgnoreList/ignoreme/tsconfig.json create mode 100644 packages/typescript-estree/tests/fixtures/projectFolderIgnoreList/includeme/file.ts create mode 100644 packages/typescript-estree/tests/fixtures/projectFolderIgnoreList/includeme/tsconfig.json diff --git a/packages/experimental-utils/src/ts-eslint/ParserOptions.ts b/packages/experimental-utils/src/ts-eslint/ParserOptions.ts index d6bf57bb585..0ff55c81c56 100644 --- a/packages/experimental-utils/src/ts-eslint/ParserOptions.ts +++ b/packages/experimental-utils/src/ts-eslint/ParserOptions.ts @@ -16,6 +16,7 @@ interface ParserOptions { loc?: boolean; noWatch?: boolean; project?: string | string[]; + projectFolderIgnoreList?: (string | RegExp)[]; range?: boolean; sourceType?: 'script' | 'module'; tokens?: boolean; diff --git a/packages/parser/README.md b/packages/parser/README.md index cf69891fb7a..70cb1cb2ca2 100644 --- a/packages/parser/README.md +++ b/packages/parser/README.md @@ -54,6 +54,7 @@ interface ParserOptions { jsx?: boolean; }; project?: string | string[]; + projectFolderIgnoreList?: (string | RegExp)[]; tsconfigRootDir?: string; extraFileExtensions?: string[]; warnOnUnsupportedTypeScriptVersion?: boolean; @@ -118,26 +119,36 @@ This option allows you to provide a path to your project's `tsconfig.json`. **Th } ``` -### `tsconfigRootDir` +### `parserOptions.tsconfigRootDir` Default `undefined`. This option allows you to provide the root directory for relative tsconfig paths specified in the `project` option above. -### `extraFileExtensions` +### `parserOptions.projectFolderIgnoreList` + +Default `["/node_modules/"]`. + +This option allows you to ignore folders from being included in your provided list of `project`s. +Any resolved project path that matches one or more of the provided regular expressions will be removed from the list. +This is useful if you have configured glob patterns, but want to make sure you ignore certain folders. + +For example, by default it will ensure that a glob like `./**/tsconfig.json` will not match any `tsconfig`s within your `node_modules` folder (some npm packages do not exclude their source files from their published packages). + +### `parserOptions.extraFileExtensions` Default `undefined`. This option allows you to provide one or more additional file extensions which should be considered in the TypeScript Program compilation. The default extensions are `.ts`, `.tsx`, `.js`, and `.jsx`. Add extensions starting with `.`, followed by the file extension. E.g. for a `.vue` file use `"extraFileExtensions: [".vue"]`. -### `warnOnUnsupportedTypeScriptVersion` +### `parserOptions.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 -### `createDefaultProgram` +### `parserOptions.createDefaultProgram` Default `false`. diff --git a/packages/typescript-estree/README.md b/packages/typescript-estree/README.md index 54706cf47f9..df2492c56b3 100644 --- a/packages/typescript-estree/README.md +++ b/packages/typescript-estree/README.md @@ -182,6 +182,16 @@ interface ParseAndGenerateServicesOptions extends ParseOptions { */ project?: string | string[]; + /** + * If you provide a glob (or globs) to the project option, you can use this option to blacklist + * certain folders from being matched by the globs. + * Any project path that matches one or more of the provided regular expressions will be removed from the list. + * + * Accepts an array of strings that are passed to new RegExp(), or an array of regular expressions. + * By default, this is set to ["/node_modules/"] + */ + projectFolderIgnoreList?: (string | RegExp)[]; + /** * The absolute path to the root directory for all provided `project`s. */ @@ -205,6 +215,7 @@ const PARSE_AND_GENERATE_SERVICES_DEFAULT_OPTIONS: ParseOptions = { extraFileExtensions: [], preserveNodeMaps: false, // or true, if you do not set this, but pass `project` project: undefined, + projectFolderIgnoreList: ['/node_modules/'], tsconfigRootDir: process.cwd(), }; diff --git a/packages/typescript-estree/src/parser-options.ts b/packages/typescript-estree/src/parser-options.ts index 73ea6ba3c04..c55ace5adb0 100644 --- a/packages/typescript-estree/src/parser-options.ts +++ b/packages/typescript-estree/src/parser-options.ts @@ -29,28 +29,23 @@ export interface Extra { // MAKE SURE THIS IS KEPT IN SYNC WITH THE README // //////////////////////////////////////////////////// -export interface TSESTreeOptions { +interface ParseOptions { /** * create a top-level comments array containing all comments */ comment?: boolean; /** - * For convenience: - * - true === ['typescript-eslint'] - * - false === [] - * * An array of modules to turn explicit debugging on for. * - 'typescript-eslint' is the same as setting the env var `DEBUG=typescript-eslint:*` * - 'eslint' is the same as setting the env var `DEBUG=eslint:*` * - 'typescript' is the same as setting `extendedDiagnostics: true` in your tsconfig compilerOptions + * + * For convenience, also supports a boolean: + * - true === ['typescript-eslint'] + * - false === [] */ - debugLevel?: boolean | DebugModule[]; - - /** - * Causes the parser to error if the TypeScript compiler returns any unexpected syntax/semantic errors. - */ - errorOnTypeScriptSyntacticAndSemanticIssues?: boolean; + debugLevel?: boolean | ('typescript-eslint' | 'eslint' | 'typescript')[]; /** * Cause the parser to error if it encounters an unknown AST node type (useful for testing). @@ -59,14 +54,7 @@ export interface TSESTreeOptions { errorOnUnknownASTType?: boolean; /** - * When `project` is provided, this controls the non-standard file extensions which will be parsed. - * It accepts an array of file extensions, each preceded by a `.`. - */ - extraFileExtensions?: string[]; - - /** - * Absolute (or relative to `tsconfigRootDir`) path to the file being parsed. - * When `project` is provided, this is required, as it is used to fetch the file from the TypeScript compiler's cache. + * Absolute (or relative to `cwd`) path to the file being parsed. */ filePath?: string; @@ -95,6 +83,45 @@ export interface TSESTreeOptions { */ loggerFn?: Function | false; + /** + * Controls whether the `range` property is included on AST nodes. + * The `range` property is a [number, number] which indicates the start/end index of the node in the file contents. + * This is similar to the `loc` property, except this is the absolute index. + */ + range?: boolean; + + /** + * Set to true to create a top-level array containing all tokens from the file. + */ + tokens?: boolean; + + /* + * The JSX AST changed the node type for string literals + * inside a JSX Element from `Literal` to `JSXText`. + * When value is `true`, these nodes will be parsed as type `JSXText`. + * When value is `false`, these nodes will be parsed as type `Literal`. + */ + useJSXTextNode?: boolean; +} + +interface ParseAndGenerateServicesOptions extends ParseOptions { + /** + * Causes the parser to error if the TypeScript compiler returns any unexpected syntax/semantic errors. + */ + errorOnTypeScriptSyntacticAndSemanticIssues?: boolean; + + /** + * When `project` is provided, this controls the non-standard file extensions which will be parsed. + * It accepts an array of file extensions, each preceded by a `.`. + */ + extraFileExtensions?: string[]; + + /** + * Absolute (or relative to `tsconfigRootDir`) path to the file being parsed. + * When `project` is provided, this is required, as it is used to fetch the file from the TypeScript compiler's cache. + */ + filePath?: string; + /** * Allows the user to control whether or not two-way AST node maps are preserved * during the AST conversion process. @@ -114,30 +141,20 @@ export interface TSESTreeOptions { project?: string | string[]; /** - * Controls whether the `range` property is included on AST nodes. - * The `range` property is a [number, number] which indicates the start/end index of the node in the file contents. - * This is similar to the `loc` property, except this is the absolute index. - */ - range?: boolean; - - /** - * Set to true to create a top-level array containing all tokens from the file. + * If you provide a glob (or globs) to the project option, you can use this option to blacklist + * certain folders from being matched by the globs. + * Any project path that matches one or more of the provided regular expressions will be removed from the list. + * + * Accepts an array of strings that are passed to new RegExp(), or an array of regular expressions. + * By default, this is set to ["/node_modules/"] */ - tokens?: boolean; + projectFolderIgnoreList?: (string | RegExp)[]; /** * The absolute path to the root directory for all provided `project`s. */ tsconfigRootDir?: string; - /* - * The JSX AST changed the node type for string literals - * inside a JSX Element from `Literal` to `JSXText`. - * When value is `true`, these nodes will be parsed as type `JSXText`. - * When value is `false`, these nodes will be parsed as type `Literal`. - */ - useJSXTextNode?: boolean; - /** *************************************************************************************** * IT IS RECOMMENDED THAT YOU DO NOT USE THIS OPTION, AS IT CAUSES PERFORMANCE ISSUES. * @@ -150,6 +167,8 @@ export interface TSESTreeOptions { createDefaultProgram?: boolean; } +export type TSESTreeOptions = ParseAndGenerateServicesOptions; + // This lets us use generics to type the return value, and removes the need to // handle the undefined type in the get method export interface ParserWeakMap { diff --git a/packages/typescript-estree/src/parser.ts b/packages/typescript-estree/src/parser.ts index b5e75d15559..5e3e9b90261 100644 --- a/packages/typescript-estree/src/parser.ts +++ b/packages/typescript-estree/src/parser.ts @@ -14,6 +14,8 @@ import { getFirstSemanticOrSyntacticError } from './semantic-or-syntactic-errors import { TSESTree } from './ts-estree'; import { ensureAbsolutePath } from './create-program/shared'; +const log = debug('typescript-eslint:typescript-estree:parser'); + /** * This needs to be kept in sync with the top-level README.md in the * typescript-eslint monorepo @@ -111,6 +113,74 @@ function resetExtra(): void { }; } +/** + * Normalizes, sanitizes, resolves and filters the provided + */ +function prepareAndTransformProjects( + projectsInput: string | string[] | undefined, + ignoreListInput: (string | RegExp)[] | undefined, +): string[] { + let projects: string[] = []; + + // Normalize and sanitize the project paths + if (typeof projectsInput === 'string') { + projects.push(projectsInput); + } else if (Array.isArray(projectsInput)) { + for (const project of projectsInput) { + if (typeof project === 'string') { + projects.push(project); + } + } + } + + if (projects.length === 0) { + return projects; + } + + // Transform glob patterns into paths + projects = projects.reduce( + (projects, project) => + projects.concat( + isGlob(project) + ? globSync(project, { + cwd: extra.tsconfigRootDir, + }) + : project, + ), + [], + ); + + // Normalize and sanitize the ignore regex list + const ignoreRegexes: RegExp[] = []; + if (Array.isArray(ignoreListInput)) { + for (const ignore of ignoreListInput) { + if (ignore instanceof RegExp) { + ignoreRegexes.push(ignore); + } else if (typeof ignore === 'string') { + ignoreRegexes.push(new RegExp(ignore)); + } + } + } else { + ignoreRegexes.push(/\/node_modules\//); + } + + // Remove any paths that match the ignore list + const filtered = projects.filter(project => { + for (const ignore of ignoreRegexes) { + if (ignore.test(project)) { + return false; + } + } + + return true; + }); + + log('parserOptions.project matched projects: %s', projects); + log('ignore list applied to parserOptions.project: %s', filtered); + + return filtered; +} + function applyParserOptionsToExtra(options: TSESTreeOptions): void { /** * Configure Debug logging @@ -205,34 +275,18 @@ function applyParserOptionsToExtra(options: TSESTreeOptions): void { extra.log = Function.prototype; } - if (typeof options.project === 'string') { - extra.projects = [options.project]; - } else if ( - Array.isArray(options.project) && - options.project.every(projectPath => typeof projectPath === 'string') - ) { - extra.projects = options.project; - } - if (typeof options.tsconfigRootDir === 'string') { extra.tsconfigRootDir = options.tsconfigRootDir; } + + // NOTE - ensureAbsolutePath relies upon having the correct tsconfigRootDir in extra extra.filePath = ensureAbsolutePath(extra.filePath, extra); - // Transform glob patterns into paths - if (extra.projects) { - extra.projects = extra.projects.reduce( - (projects, project) => - projects.concat( - isGlob(project) - ? globSync(project, { - cwd: extra.tsconfigRootDir || process.cwd(), - }) - : project, - ), - [], - ); - } + // NOTE - prepareAndTransformProjects relies upon having the correct tsconfigRootDir in extra + extra.projects = prepareAndTransformProjects( + options.project, + options.projectFolderIgnoreList, + ); if ( Array.isArray(options.extraFileExtensions) && diff --git a/packages/typescript-estree/tests/fixtures/projectFolderIgnoreList/ignoreme/file.ts b/packages/typescript-estree/tests/fixtures/projectFolderIgnoreList/ignoreme/file.ts new file mode 100644 index 00000000000..ad1d380d6cc --- /dev/null +++ b/packages/typescript-estree/tests/fixtures/projectFolderIgnoreList/ignoreme/file.ts @@ -0,0 +1 @@ +export const x = 1; diff --git a/packages/typescript-estree/tests/fixtures/projectFolderIgnoreList/ignoreme/tsconfig.json b/packages/typescript-estree/tests/fixtures/projectFolderIgnoreList/ignoreme/tsconfig.json new file mode 100644 index 00000000000..a01e8a941e8 --- /dev/null +++ b/packages/typescript-estree/tests/fixtures/projectFolderIgnoreList/ignoreme/tsconfig.json @@ -0,0 +1,3 @@ +{ + "include": ["./file.ts"] +} diff --git a/packages/typescript-estree/tests/fixtures/projectFolderIgnoreList/includeme/file.ts b/packages/typescript-estree/tests/fixtures/projectFolderIgnoreList/includeme/file.ts new file mode 100644 index 00000000000..25dfdc46133 --- /dev/null +++ b/packages/typescript-estree/tests/fixtures/projectFolderIgnoreList/includeme/file.ts @@ -0,0 +1 @@ +export const x = 2; diff --git a/packages/typescript-estree/tests/fixtures/projectFolderIgnoreList/includeme/tsconfig.json b/packages/typescript-estree/tests/fixtures/projectFolderIgnoreList/includeme/tsconfig.json new file mode 100644 index 00000000000..a01e8a941e8 --- /dev/null +++ b/packages/typescript-estree/tests/fixtures/projectFolderIgnoreList/includeme/tsconfig.json @@ -0,0 +1,3 @@ +{ + "include": ["./file.ts"] +} diff --git a/packages/typescript-estree/tests/lib/parse.ts b/packages/typescript-estree/tests/lib/parse.ts index 1e6a1e97236..c6435508eac 100644 --- a/packages/typescript-estree/tests/lib/parse.ts +++ b/packages/typescript-estree/tests/lib/parse.ts @@ -557,4 +557,49 @@ describe('parse()', () => { ); }); }); + + describe('projectFolderIgnoreList', () => { + beforeEach(() => { + parser.clearCaches(); + }); + + const PROJECT_DIR = resolve(FIXTURES_DIR, '../projectFolderIgnoreList'); + const code = 'var a = true'; + const config: TSESTreeOptions = { + comment: true, + tokens: true, + range: true, + loc: true, + tsconfigRootDir: PROJECT_DIR, + project: './**/tsconfig.json', + }; + + const testParse = ( + filePath: 'ignoreme' | 'includeme', + projectFolderIgnoreList: TSESTreeOptions['projectFolderIgnoreList'] = [], + ) => (): void => { + parser.parseAndGenerateServices(code, { + ...config, + projectFolderIgnoreList, + filePath: join(PROJECT_DIR, filePath, './file.ts'), + }); + }; + + it('ignores nothing when given nothing', () => { + expect(testParse('ignoreme')).not.toThrow(); + expect(testParse('includeme')).not.toThrow(); + }); + + it('ignores a folder when given a string regexp', () => { + const ignore = ['/ignoreme/']; + expect(testParse('ignoreme', ignore)).toThrow(); + expect(testParse('includeme', ignore)).not.toThrow(); + }); + + it('ignores a folder when given a RegExp', () => { + const ignore = [/\/ignoreme\//]; + expect(testParse('ignoreme', ignore)).toThrow(); + expect(testParse('includeme', ignore)).not.toThrow(); + }); + }); }); diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json index 2d9938163ea..40defd4b3b8 100644 --- a/tsconfig.eslint.json +++ b/tsconfig.eslint.json @@ -1,4 +1,4 @@ { "extends": "./tsconfig.base.json", - "include": ["tests/**/*.ts", "tools/**/*.ts"] + "include": ["tests/**/*.ts", "tools/**/*.ts", ".eslintrc.js"] }