From 3bd38caf4d8d2ca48490f241788ee174805f57b1 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Mon, 24 Oct 2022 14:02:14 -0400 Subject: [PATCH 01/20] chore(website): fix Options heading level for no-empty-interface docs (#5870) --- .../docs/rules/no-empty-interface.md | 2 +- packages/eslint-plugin/tests/docs.test.ts | 29 ++++++++++++++++--- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/no-empty-interface.md b/packages/eslint-plugin/docs/rules/no-empty-interface.md index 194a6e6a654b..699ebccb64a6 100644 --- a/packages/eslint-plugin/docs/rules/no-empty-interface.md +++ b/packages/eslint-plugin/docs/rules/no-empty-interface.md @@ -48,7 +48,7 @@ interface Baz extends Foo, Bar {} -### Options +## Options This rule accepts a single object option with the following default configuration: diff --git a/packages/eslint-plugin/tests/docs.test.ts b/packages/eslint-plugin/tests/docs.test.ts index 9442ed366a38..e7872a962c9e 100644 --- a/packages/eslint-plugin/tests/docs.test.ts +++ b/packages/eslint-plugin/tests/docs.test.ts @@ -26,11 +26,15 @@ function tokenIs( return token.type === type; } -function tokenIsH2(token: marked.Token): token is marked.Tokens.Heading { +function tokenIsHeading(token: marked.Token): token is marked.Tokens.Heading { + return tokenIs(token, 'heading'); +} + +function tokenIsH2( + token: marked.Token, +): token is marked.Tokens.Heading & { depth: 2 } { return ( - tokenIs(token, 'heading') && - token.depth === 2 && - !/[a-z]+: /.test(token.text) + tokenIsHeading(token) && token.depth === 2 && !/[a-z]+: /.test(token.text) ); } @@ -93,6 +97,23 @@ describe('Validating rule docs', () => { expect(header.text).toBe(titleCase(header.text)), ); }); + + const importantHeadings = new Set([ + 'How to Use', + 'Options', + 'Related To', + 'When Not To Use It', + ]); + + test('important headings must be h2s', () => { + const headers = tokens.filter(tokenIsHeading); + + for (const header of headers) { + if (importantHeadings.has(header.raw.replace(/#/g, '').trim())) { + expect(header.depth).toBe(2); + } + } + }); }); } }); From 1eaae09ecca359f366b94f6a04665403f48b05c7 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Mon, 24 Oct 2022 15:09:42 -0400 Subject: [PATCH 02/20] feat(eslint-plugin): [member-ordering] add natural sort order (#5662) * [WIP] feat(eslint-plugin): [member-ordering] add natural sort order * Fix yarn.lock and split option on case sensitivity * Document it too * Remove last todos * Move member-ordering sub-tests into sub-dirs --- .../docs/rules/member-ordering.md | 18 ++- packages/eslint-plugin/package.json | 2 + .../src/rules/member-ordering.ts | 53 +++++-- ...habetically-case-insensitive-order.test.ts | 6 +- ...mber-ordering-alphabetically-order.test.ts | 6 +- ...ing-natural-case-insensitive-order.test.ts | 135 ++++++++++++++++ .../member-ordering-natural-order.test.ts | 144 ++++++++++++++++++ yarn.lock | 10 ++ 8 files changed, 352 insertions(+), 22 deletions(-) rename packages/eslint-plugin/tests/rules/{ => member-ordering}/member-ordering-alphabetically-case-insensitive-order.test.ts (98%) rename packages/eslint-plugin/tests/rules/{ => member-ordering}/member-ordering-alphabetically-order.test.ts (99%) create mode 100644 packages/eslint-plugin/tests/rules/member-ordering/member-ordering-natural-case-insensitive-order.test.ts create mode 100644 packages/eslint-plugin/tests/rules/member-ordering/member-ordering-natural-order.test.ts diff --git a/packages/eslint-plugin/docs/rules/member-ordering.md b/packages/eslint-plugin/docs/rules/member-ordering.md index 9a6ca8d5dbc8..7adde7ba9a63 100644 --- a/packages/eslint-plugin/docs/rules/member-ordering.md +++ b/packages/eslint-plugin/docs/rules/member-ordering.md @@ -24,7 +24,12 @@ type OrderConfig = MemberType[] | SortedOrderConfig | 'never'; interface SortedOrderConfig { memberTypes?: MemberType[] | 'never'; - order: 'alphabetically' | 'alphabetically-case-insensitive' | 'as-written'; + order: + | 'alphabetically' + | 'alphabetically-case-insensitive' + | 'as-written' + | 'natural' + | 'natural-case-insensitive'; } // See below for the more specific MemberType strings @@ -56,6 +61,17 @@ The supported member attributes are, in order: Member attributes may be joined with a `'-'` to combine into more specific groups. For example, `'public-field'` would come before `'private-field'`. +### Orders + +The `order` value specifies what order members should be within a group. +It defaults to `as-written`, meaning any order is fine. +Other allowed values are: + +- `alphabetically`: Sorted in a-z alphabetical order, directly using string `<` comparison (so `B` comes before `a`) +- `alphabetically-case-insensitive`: Sorted in a-z alphabetical order, ignoring case (so `a` comes before `B`) +- `natural`: Same as `alphabetically`, but using [`natural-compare-lite`](https://github.com/litejs/natural-compare-lite) for more friendly sorting of numbers +- `natural-case-insensitive`: Same as `alphabetically-case-insensitive`, but using [`natural-compare-lite`](https://github.com/litejs/natural-compare-lite) for more friendly sorting of numbers + ### Default configuration The default configuration looks as follows: diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index c977c64b8bc0..eb1c777d1154 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -49,6 +49,7 @@ "@typescript-eslint/utils": "5.41.0", "debug": "^4.3.4", "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", "regexpp": "^3.2.0", "semver": "^7.3.7", "tsutils": "^3.21.0" @@ -57,6 +58,7 @@ "@types/debug": "*", "@types/json-schema": "*", "@types/marked": "*", + "@types/natural-compare-lite": "^1.4.0", "@types/prettier": "*", "chalk": "^5.0.1", "json-schema": "*", diff --git a/packages/eslint-plugin/src/rules/member-ordering.ts b/packages/eslint-plugin/src/rules/member-ordering.ts index 66fe9b623342..3892c989bc9a 100644 --- a/packages/eslint-plugin/src/rules/member-ordering.ts +++ b/packages/eslint-plugin/src/rules/member-ordering.ts @@ -1,5 +1,6 @@ import type { JSONSchema, TSESLint, TSESTree } from '@typescript-eslint/utils'; import { AST_NODE_TYPES } from '@typescript-eslint/utils'; +import naturalCompare from 'natural-compare-lite'; import * as util from '../util'; @@ -34,10 +35,13 @@ type BaseMemberType = type MemberType = BaseMemberType | BaseMemberType[]; -type Order = +type AlphabeticalOrder = | 'alphabetically' | 'alphabetically-case-insensitive' - | 'as-written'; + | 'natural' + | 'natural-case-insensitive'; + +type Order = AlphabeticalOrder | 'as-written'; interface SortedOrderConfig { memberTypes?: MemberType[] | 'never'; @@ -87,7 +91,13 @@ const objectConfig = (memberTypes: MemberType[]): JSONSchema.JSONSchema4 => ({ }, order: { type: 'string', - enum: ['alphabetically', 'alphabetically-case-insensitive', 'as-written'], + enum: [ + 'alphabetically', + 'alphabetically-case-insensitive', + 'as-written', + 'natural', + 'natural-case-insensitive', + ], }, }, additionalProperties: false, @@ -629,7 +639,7 @@ export default util.createRule({ */ function checkAlphaSort( members: Member[], - caseSensitive: boolean, + order: AlphabeticalOrder, ): boolean { let previousName = ''; let isCorrectlySorted = true; @@ -640,11 +650,7 @@ export default util.createRule({ // Note: Not all members have names if (name) { - if ( - caseSensitive - ? name < previousName - : name.toLowerCase() < previousName.toLowerCase() - ) { + if (naturalOutOfOrder(name, previousName, order)) { context.report({ node: member, messageId: 'incorrectOrder', @@ -664,6 +670,25 @@ export default util.createRule({ return isCorrectlySorted; } + function naturalOutOfOrder( + name: string, + previousName: string, + order: AlphabeticalOrder, + ): boolean { + switch (order) { + case 'alphabetically': + return name < previousName; + case 'alphabetically-case-insensitive': + return name.toLowerCase() < previousName.toLowerCase(); + case 'natural': + return naturalCompare(name, previousName) !== 1; + case 'natural-case-insensitive': + return ( + naturalCompare(name.toLowerCase(), previousName.toLowerCase()) !== 1 + ); + } + } + /** * Validates if all members are correctly sorted. * @@ -681,7 +706,7 @@ export default util.createRule({ } // Standardize config - let order: Order | null = null; + let order: Order | undefined; let memberTypes; if (Array.isArray(orderConfig)) { @@ -691,9 +716,7 @@ export default util.createRule({ memberTypes = orderConfig.memberTypes; } - const hasAlphaSort = order?.startsWith('alphabetically'); - const alphaSortIsCaseSensitive = - order !== 'alphabetically-case-insensitive'; + const hasAlphaSort = !!(order && order !== 'as-written'); // Check order if (Array.isArray(memberTypes)) { @@ -706,11 +729,11 @@ export default util.createRule({ if (hasAlphaSort) { grouped.some( groupMember => - !checkAlphaSort(groupMember, alphaSortIsCaseSensitive), + !checkAlphaSort(groupMember, order as AlphabeticalOrder), ); } } else if (hasAlphaSort) { - checkAlphaSort(members, alphaSortIsCaseSensitive); + checkAlphaSort(members, order as AlphabeticalOrder); } } diff --git a/packages/eslint-plugin/tests/rules/member-ordering-alphabetically-case-insensitive-order.test.ts b/packages/eslint-plugin/tests/rules/member-ordering/member-ordering-alphabetically-case-insensitive-order.test.ts similarity index 98% rename from packages/eslint-plugin/tests/rules/member-ordering-alphabetically-case-insensitive-order.test.ts rename to packages/eslint-plugin/tests/rules/member-ordering/member-ordering-alphabetically-case-insensitive-order.test.ts index d2600b17dc85..3eecfc999c9a 100644 --- a/packages/eslint-plugin/tests/rules/member-ordering-alphabetically-case-insensitive-order.test.ts +++ b/packages/eslint-plugin/tests/rules/member-ordering/member-ordering-alphabetically-case-insensitive-order.test.ts @@ -1,8 +1,8 @@ import type { TSESLint } from '@typescript-eslint/utils'; -import type { MessageIds, Options } from '../../src/rules/member-ordering'; -import rule, { defaultOrder } from '../../src/rules/member-ordering'; -import { RuleTester } from '../RuleTester'; +import type { MessageIds, Options } from '../../../src/rules/member-ordering'; +import rule, { defaultOrder } from '../../../src/rules/member-ordering'; +import { RuleTester } from '../../RuleTester'; const ruleTester = new RuleTester({ parser: '@typescript-eslint/parser', diff --git a/packages/eslint-plugin/tests/rules/member-ordering-alphabetically-order.test.ts b/packages/eslint-plugin/tests/rules/member-ordering/member-ordering-alphabetically-order.test.ts similarity index 99% rename from packages/eslint-plugin/tests/rules/member-ordering-alphabetically-order.test.ts rename to packages/eslint-plugin/tests/rules/member-ordering/member-ordering-alphabetically-order.test.ts index 63ef55408e7a..afa1a5df810e 100644 --- a/packages/eslint-plugin/tests/rules/member-ordering-alphabetically-order.test.ts +++ b/packages/eslint-plugin/tests/rules/member-ordering/member-ordering-alphabetically-order.test.ts @@ -1,8 +1,8 @@ import type { TSESLint } from '@typescript-eslint/utils'; -import type { MessageIds, Options } from '../../src/rules/member-ordering'; -import rule, { defaultOrder } from '../../src/rules/member-ordering'; -import { RuleTester } from '../RuleTester'; +import type { MessageIds, Options } from '../../../src/rules/member-ordering'; +import rule, { defaultOrder } from '../../../src/rules/member-ordering'; +import { RuleTester } from '../../RuleTester'; const ruleTester = new RuleTester({ parser: '@typescript-eslint/parser', diff --git a/packages/eslint-plugin/tests/rules/member-ordering/member-ordering-natural-case-insensitive-order.test.ts b/packages/eslint-plugin/tests/rules/member-ordering/member-ordering-natural-case-insensitive-order.test.ts new file mode 100644 index 000000000000..782fee826d5a --- /dev/null +++ b/packages/eslint-plugin/tests/rules/member-ordering/member-ordering-natural-case-insensitive-order.test.ts @@ -0,0 +1,135 @@ +import rule from '../../../src/rules/member-ordering'; +import { RuleTester } from '../../RuleTester'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', +}); + +ruleTester.run('member-ordering-natural-order', rule, { + valid: [ + { + code: ` +interface Example { + 1: number; + 5: number; + 10: number; +} + `, + options: [ + { + default: { + order: 'natural-case-insensitive', + }, + }, + ], + }, + { + code: ` +interface Example { + new (): unknown; + + a1(): void; + a5(): void; + a10(): void; + B1(): void; + B5(): void; + B10(): void; + + a1: number; + a5: number; + a10: number; + B1: number; + B5: number; + B10: number; +} + `, + options: [ + { + default: { + memberTypes: ['constructor', 'method', 'field'], + order: 'natural-case-insensitive', + }, + }, + ], + }, + ], + invalid: [ + { + code: ` +interface Example { + 1: number; + 10: number; + 5: number; +} + `, + errors: [ + { + messageId: 'incorrectOrder', + data: { + beforeMember: 10, + member: 5, + }, + line: 5, + column: 3, + }, + ], + options: [ + { + default: { + order: 'natural-case-insensitive', + }, + }, + ], + }, + + { + code: ` +interface Example { + new (): unknown; + + a1(): void; + a10(): void; + a5(): void; + B5(): void; + B10(): void; + B1(): void; + + a5: number; + a10: number; + B1: number; + a1: number; + B5: number; + B10: number; +} + `, + errors: [ + { + column: 3, + data: { + beforeMember: 'a10', + member: 'a5', + }, + line: 7, + messageId: 'incorrectOrder', + }, + { + column: 3, + data: { + beforeMember: 'B10', + member: 'B1', + }, + line: 10, + messageId: 'incorrectOrder', + }, + ], + options: [ + { + default: { + memberTypes: ['constructor', 'method', 'field'], + order: 'natural-case-insensitive', + }, + }, + ], + }, + ], +}); diff --git a/packages/eslint-plugin/tests/rules/member-ordering/member-ordering-natural-order.test.ts b/packages/eslint-plugin/tests/rules/member-ordering/member-ordering-natural-order.test.ts new file mode 100644 index 000000000000..c34677a81c7a --- /dev/null +++ b/packages/eslint-plugin/tests/rules/member-ordering/member-ordering-natural-order.test.ts @@ -0,0 +1,144 @@ +import rule from '../../../src/rules/member-ordering'; +import { RuleTester } from '../../RuleTester'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', +}); + +ruleTester.run('member-ordering-natural-order', rule, { + valid: [ + { + code: ` +interface Example { + 1: number; + 5: number; + 10: number; +} + `, + options: [ + { + default: { + order: 'natural', + }, + }, + ], + }, + { + code: ` +interface Example { + new (): unknown; + + B1(): void; + B5(): void; + B10(): void; + a1(): void; + a5(): void; + a10(): void; + + B1: number; + B5: number; + B10: number; + a1: number; + a5: number; + a10: number; +} + `, + options: [ + { + default: { + memberTypes: ['constructor', 'method', 'field'], + order: 'natural', + }, + }, + ], + }, + ], + invalid: [ + { + code: ` +interface Example { + 1: number; + 10: number; + 5: number; +} + `, + errors: [ + { + messageId: 'incorrectOrder', + data: { + beforeMember: 10, + member: 5, + }, + line: 5, + column: 3, + }, + ], + options: [ + { + default: { + order: 'natural', + }, + }, + ], + }, + + { + code: ` +interface Example { + new (): unknown; + + a1(): void; + a10(): void; + a5(): void; + B5(): void; + B10(): void; + B1(): void; + + a5: number; + a10: number; + B1: number; + a1: number; + B5: number; + B10: number; +} + `, + errors: [ + { + column: 3, + data: { + beforeMember: 'a10', + member: 'a5', + }, + line: 7, + messageId: 'incorrectOrder', + }, + { + column: 3, + data: { + beforeMember: 'a5', + member: 'B5', + }, + line: 8, + messageId: 'incorrectOrder', + }, + { + column: 3, + data: { + beforeMember: 'B10', + member: 'B1', + }, + line: 10, + messageId: 'incorrectOrder', + }, + ], + options: [ + { + default: { + memberTypes: ['constructor', 'method', 'field'], + order: 'natural', + }, + }, + ], + }, + ], +}); diff --git a/yarn.lock b/yarn.lock index 2c88e05570a2..1e719261af60 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4112,6 +4112,11 @@ resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197" integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== +"@types/natural-compare-lite@^1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@types/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#90724682da3c304dd8d643b4e9ba00f53f29d454" + integrity sha512-ZDcj/yWsRPacqKPpCExWWFq9X1JQwEOfHsu8deN1Qfa6M02z6tN4DK6AMf2IkM7709gp3QW6Bo7m2NFDhA485w== + "@types/ncp@^2.0.5": version "2.0.5" resolved "https://registry.yarnpkg.com/@types/ncp/-/ncp-2.0.5.tgz#5c53b229a321946102a188b603306162137f4fb9" @@ -10327,6 +10332,11 @@ nanoid@^3.3.4: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== +natural-compare-lite@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4" + integrity sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g== + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" From 67744db31f61acab14b5fe027fbc2844ba198c97 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Mon, 24 Oct 2022 17:43:07 -0400 Subject: [PATCH 03/20] feat(typescript-estree): clarify docs and error for program project without matching TSConfig (#5762) * feat(typescript-estree): clarify docs and error for program project without matching TSConfig * lil cspell nit * Update docs/linting/TROUBLESHOOTING.md Co-authored-by: Andrew Churchill * Apply suggestions from code review Co-authored-by: Brad Zacher * Resolved a few docs comments * A few test fixes * Remove period * Apply suggestions from code review Co-authored-by: Brad Zacher * Link to FAQ article * Reordered docs, and updated tests Co-authored-by: Andrew Churchill Co-authored-by: Brad Zacher --- docs/linting/TROUBLESHOOTING.md | 44 ++++++- docs/linting/TYPED_LINTING.md | 19 +-- .../create-program/createProjectProgram.ts | 113 +++++++++++------- .../invalidFileErrors/tsconfig.extra.json | 1 + .../lib/__snapshots__/parse.test.ts.snap | 84 ++++++++----- .../typescript-estree/tests/lib/parse.test.ts | 57 +++++++-- .../tests/lib/semanticInfo.test.ts | 4 +- 7 files changed, 219 insertions(+), 103 deletions(-) create mode 100644 packages/typescript-estree/tests/fixtures/invalidFileErrors/tsconfig.extra.json diff --git a/docs/linting/TROUBLESHOOTING.md b/docs/linting/TROUBLESHOOTING.md index f37f8780a332..e477267c476f 100644 --- a/docs/linting/TROUBLESHOOTING.md +++ b/docs/linting/TROUBLESHOOTING.md @@ -28,12 +28,48 @@ If you don't find an existing extension rule, or the extension rule doesn't work > We release a new version our tooling every week. > _Please_ ensure that you [check our the latest list of "extension" rules](https://typescript-eslint.io/rules/#extension-rules) **_before_** filing an issue. -## I get errors telling me "The file must be included in at least one of the projects provided" +## I get errors telling me "ESLint was configured to run ... However, that TSConfig does not / none of those TSConfigs include this file" + +### Fixing the Error + +- If you **do not** want to lint the file: + - Use [one of the options ESLint offers](https://eslint.org/docs/latest/user-guide/configuring/ignoring-code) to ignore files, namely a `.eslintignore` file, or `ignorePatterns` config. +- If you **do** want to lint the file: + - If you **do not** want to lint the file with [type-aware linting](./TYPED_LINTING.md): + - Use [ESLint's `overrides` configuration](https://eslint.org/docs/latest/user-guide/configuring/configuration-files#configuration-based-on-glob-patterns) to configure the file to not be parsed with type information. + - A popular setup is to omit the above additions from top-level configuration and only apply them to TypeScript files via an override. + - Alternatively, you can add `parserOptions: { project: null }` to an override for the files you wish to exclude. Note that `{ project: undefined }` will not work. + - If you **do** want to lint the file with [type-aware linting](./TYPED_LINTING.md): + - Check the `include` option of each of the tsconfigs that you provide to `parserOptions.project` - you must ensure that all files match an `include` glob, or else our tooling will not be able to find it. + - If your file shouldn't be a part of one of your existing tsconfigs (for example, it is a script/tool local to the repo), then consider creating a new tsconfig (we advise calling it `tsconfig.eslint.json`) in your project root which lists this file in its `include`. For an example of this, you can check out the configuration we use in this repo: + - [`tsconfig.eslint.json`](https://github.com/typescript-eslint/typescript-eslint/blob/main/tsconfig.eslint.json) + - [`.eslintrc.js`](https://github.com/typescript-eslint/typescript-eslint/blob/main/.eslintrc.js) + +### More Details + +This error may appear from the combination of two things: + +- The ESLint configuration for the source file specifies at least one TSConfig file in `parserOptions.project` +- None of those TSConfig files includes the source file being linted + - Note that files with the same name and different extension may not be recognized by TypeScript: see [`parserOptions.project` docs](https://github.com/typescript-eslint/typescript-eslint/tree/main/packages/parser#parseroptionsproject) -This error means that the file that's being linted is not included in any of the TSConfig files you provided us. -This happens when users have test files, config files, or similar that are not included. +When TSConfig files are specified for parsing a source file, `@typescript-eslint/parser` will use the first TSConfig that is able to include that source file (per [aka.ms/tsconfig#include](https://www.typescriptlang.org/tsconfig#include)) to generate type information. +However, if no specified TSConfig includes the source file, the parser won't be able to generate type information. + +This error most commonly happens on config files or similar that are not included in their project TSConfig(s). +For example, many projects have files like: + +- An `.eslintrc.cjs` with `parserOptions.project: ["./tsconfig.json"]` +- A `tsconfig.json` with `include: ["src"]` + +In that case, viewing the `.eslintrc.cjs` in an IDE with the ESLint extension will show the error notice that the file couldn't be linted because it isn't included in `tsconfig.json`. + +See our docs on [type aware linting](./TYPED_LINTING.md) for more information. + +## I get errors telling me "The file must be included in at least one of the projects provided" -See our docs on [type aware linting](./TYPED_LINTING.md#i-get-errors-telling-me-the-file-must-be-included-in-at-least-one-of-the-projects-provided) for solutions. +You're using an outdated version of `@typescript-eslint/parser`. +Update to the latest version to see a more informative version of this error message, explained [above](#i-get-errors-telling-me-eslint-was-configured-to-run--however-that-tsconfig-does-not--none-of-those-tsconfigs-include-this-file). ## I use a framework (like Vue) that requires custom file extensions, and I get errors like "You should add `parserOptions.extraFileExtensions` to your config" diff --git a/docs/linting/TYPED_LINTING.md b/docs/linting/TYPED_LINTING.md index fcf6f4b185d1..8ffb708d0e3e 100644 --- a/docs/linting/TYPED_LINTING.md +++ b/docs/linting/TYPED_LINTING.md @@ -52,23 +52,8 @@ This means that generally they usually only run a complete lint before a push, o ### I get errors telling me "The file must be included in at least one of the projects provided" -This error means that the file that's being linted is not included in any of the tsconfig files you provided us. -A lot of the time this happens when users have test files or similar that are not included in their normal tsconfigs. - -Depending on what you want to achieve: - -- If you **do not** want to lint the file: - - Use [one of the options ESLint offers](https://eslint.org/docs/user-guide/configuring#ignoring-files-and-directories) to ignore files, like a `.eslintignore` file, or `ignorePatterns` config. -- If you **do** want to lint the file: - - If you **do not** want to lint the file with [type-aware linting](./TYPED_LINTING.md): - - Use [ESLint's `overrides` configuration](https://eslint.org/docs/user-guide/configuring#configuration-based-on-glob-patterns) to configure the file to not be parsed with type information. - - A popular setup is to omit the above additions from top-level configuration and only apply them to TypeScript files via an override. - - Alternatively, you can add `parserOptions: { project: null }` to an override for the files you wish to exclude. Note that `{ project: undefined }` will not work. - - If you **do** want to lint the file with [type-aware linting](./TYPED_LINTING.md): - - Check the `include` option of each of the tsconfigs that you provide to `parserOptions.project` - you must ensure that all files match an `include` glob, or else our tooling will not be able to find it. - - If your file shouldn't be a part of one of your existing tsconfigs (for example, it is a script/tool local to the repo), then consider creating a new tsconfig (we advise calling it `tsconfig.eslint.json`) in your project root which lists this file in its `include`. For an example of this, you can check out the configuration we use in this repo: - - [`tsconfig.eslint.json`](https://github.com/typescript-eslint/typescript-eslint/blob/main/tsconfig.eslint.json) - - [`.eslintrc.js`](https://github.com/typescript-eslint/typescript-eslint/blob/main/.eslintrc.js) +You're using an outdated version of `@typescript-eslint/parser`. +Update to the latest version to see a more informative version of this error message, explained [Troubleshooting and FAQs](./TROUBLESHOOTING.md##i-get-errors-telling-me-eslint-was-configured-to-run--however-that-tsconfig-does-not--none-of-those-tsconfigs-include-this-file). ## Troubleshooting diff --git a/packages/typescript-estree/src/create-program/createProjectProgram.ts b/packages/typescript-estree/src/create-program/createProjectProgram.ts index 359c2b2dc907..9ece5e4a8273 100644 --- a/packages/typescript-estree/src/create-program/createProjectProgram.ts +++ b/packages/typescript-estree/src/create-program/createProjectProgram.ts @@ -34,65 +34,90 @@ function createProjectProgram( ): ASTAndProgram | undefined { log('Creating project program for: %s', extra.filePath); - const astAndProgram = firstDefined( - getProgramsForProjects(code, extra.filePath, extra), - currentProgram => getAstFromProgram(currentProgram, extra), + const programsForProjects = getProgramsForProjects( + code, + extra.filePath, + extra, + ); + const astAndProgram = firstDefined(programsForProjects, currentProgram => + getAstFromProgram(currentProgram, extra), ); - if (!astAndProgram && !createDefaultProgram) { - // the file was either not matched within the tsconfig, or the extension wasn't expected - const errorLines = [ - '"parserOptions.project" has been set for @typescript-eslint/parser.', - `The file does not match your project config: ${path.relative( - extra.tsconfigRootDir || process.cwd(), - extra.filePath, - )}.`, - ]; - let hasMatchedAnError = false; + // The file was either matched within the tsconfig, or we allow creating a default program + if (astAndProgram || createDefaultProgram) { + return astAndProgram; + } - const extraFileExtensions = extra.extraFileExtensions || []; + const describeFilePath = (filePath: string): string => { + const relative = path.relative( + extra.tsconfigRootDir || process.cwd(), + filePath, + ); + if (extra.tsconfigRootDir) { + return `/${relative}`; + } + return `/${relative}`; + }; - extraFileExtensions.forEach(extraExtension => { - if (!extraExtension.startsWith('.')) { - errorLines.push( - `Found unexpected extension "${extraExtension}" specified with the "extraFileExtensions" option. Did you mean ".${extraExtension}"?`, - ); - } - if (DEFAULT_EXTRA_FILE_EXTENSIONS.includes(extraExtension)) { - errorLines.push( - `You unnecessarily included the extension "${extraExtension}" with the "extraFileExtensions" option. This extension is already handled by the parser by default.`, - ); - } - }); + const describedFilePath = describeFilePath(extra.filePath); + const relativeProjects = extra.projects.map(describeFilePath); + const describedPrograms = + relativeProjects.length === 1 + ? relativeProjects[0] + : `\n${relativeProjects.map(project => `- ${project}`).join('\n')}`; + const errorLines = [ + `ESLint was configured to run on \`${describedFilePath}\` using \`parserOptions.project\`: ${describedPrograms}`, + ]; + let hasMatchedAnError = false; + + const extraFileExtensions = extra.extraFileExtensions || []; - const fileExtension = path.extname(extra.filePath); - if (!DEFAULT_EXTRA_FILE_EXTENSIONS.includes(fileExtension)) { - const nonStandardExt = `The extension for the file (${fileExtension}) is non-standard`; - if (extraFileExtensions.length > 0) { - if (!extraFileExtensions.includes(fileExtension)) { - errorLines.push( - `${nonStandardExt}. It should be added to your existing "parserOptions.extraFileExtensions".`, - ); - hasMatchedAnError = true; - } - } else { + extraFileExtensions.forEach(extraExtension => { + if (!extraExtension.startsWith('.')) { + errorLines.push( + `Found unexpected extension \`${extraExtension}\` specified with the \`parserOptions.extraFileExtensions\` option. Did you mean \`.${extraExtension}\`?`, + ); + } + if (DEFAULT_EXTRA_FILE_EXTENSIONS.includes(extraExtension)) { + errorLines.push( + `You unnecessarily included the extension \`${extraExtension}\` with the \`parserOptions.extraFileExtensions\` option. This extension is already handled by the parser by default.`, + ); + } + }); + + const fileExtension = path.extname(extra.filePath); + if (!DEFAULT_EXTRA_FILE_EXTENSIONS.includes(fileExtension)) { + const nonStandardExt = `The extension for the file (\`${fileExtension}\`) is non-standard`; + if (extraFileExtensions.length > 0) { + if (!extraFileExtensions.includes(fileExtension)) { errorLines.push( - `${nonStandardExt}. You should add "parserOptions.extraFileExtensions" to your config.`, + `${nonStandardExt}. It should be added to your existing \`parserOptions.extraFileExtensions\`.`, ); hasMatchedAnError = true; } - } - - if (!hasMatchedAnError) { + } else { errorLines.push( - 'The file must be included in at least one of the projects provided.', + `${nonStandardExt}. You should add \`parserOptions.extraFileExtensions\` to your config.`, ); + hasMatchedAnError = true; } + } - throw new Error(errorLines.join('\n')); + if (!hasMatchedAnError) { + const [describedInclusions, describedSpecifiers] = + extra.projects.length === 1 + ? ['that TSConfig does not', 'that TSConfig'] + : ['none of those TSConfigs', 'one of those TSConfigs']; + errorLines.push( + `However, ${describedInclusions} include this file. Either:`, + `- Change ESLint's list of included files to not include this file`, + `- Change ${describedSpecifiers} to include this file`, + `- Create a new TSConfig that includes this file and include it in your parserOptions.project`, + `See the TypeScript ESLint docs for more info: https://typescript-eslint.io/docs/linting/troubleshooting##i-get-errors-telling-me-eslint-was-configured-to-run--however-that-tsconfig-does-not--none-of-those-tsconfigs-include-this-file`, + ); } - return astAndProgram; + throw new Error(errorLines.join('\n')); } export { createProjectProgram }; diff --git a/packages/typescript-estree/tests/fixtures/invalidFileErrors/tsconfig.extra.json b/packages/typescript-estree/tests/fixtures/invalidFileErrors/tsconfig.extra.json new file mode 100644 index 000000000000..0967ef424bce --- /dev/null +++ b/packages/typescript-estree/tests/fixtures/invalidFileErrors/tsconfig.extra.json @@ -0,0 +1 @@ +{} diff --git a/packages/typescript-estree/tests/lib/__snapshots__/parse.test.ts.snap b/packages/typescript-estree/tests/lib/__snapshots__/parse.test.ts.snap index 9816bea29144..81341b5e35f2 100644 --- a/packages/typescript-estree/tests/lib/__snapshots__/parse.test.ts.snap +++ b/packages/typescript-estree/tests/lib/__snapshots__/parse.test.ts.snap @@ -1,59 +1,85 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`parseAndGenerateServices invalid file error messages "parserOptions.extraFileExtensions" is empty the extension does not match 1`] = ` -""parserOptions.project" has been set for @typescript-eslint/parser. -The file does not match your project config: other/unknownFileType.unknown. -The extension for the file (.unknown) is non-standard. You should add "parserOptions.extraFileExtensions" to your config." +"ESLint was configured to run on \`/other/unknownFileType.unknown\` using \`parserOptions.project\`: /tsconfig.json +The extension for the file (\`.unknown\`) is non-standard. You should add \`parserOptions.extraFileExtensions\` to your config." `; exports[`parseAndGenerateServices invalid file error messages "parserOptions.extraFileExtensions" is non-empty invalid extension 1`] = ` -""parserOptions.project" has been set for @typescript-eslint/parser. -The file does not match your project config: other/unknownFileType.unknown. -Found unexpected extension "unknown" specified with the "extraFileExtensions" option. Did you mean ".unknown"? -The extension for the file (.unknown) is non-standard. It should be added to your existing "parserOptions.extraFileExtensions"." +"ESLint was configured to run on \`/other/unknownFileType.unknown\` using \`parserOptions.project\`: /tsconfig.json +Found unexpected extension \`unknown\` specified with the \`parserOptions.extraFileExtensions\` option. Did you mean \`.unknown\`? +The extension for the file (\`.unknown\`) is non-standard. It should be added to your existing \`parserOptions.extraFileExtensions\`." `; exports[`parseAndGenerateServices invalid file error messages "parserOptions.extraFileExtensions" is non-empty the extension does not match 1`] = ` -""parserOptions.project" has been set for @typescript-eslint/parser. -The file does not match your project config: other/unknownFileType.unknown. -The extension for the file (.unknown) is non-standard. It should be added to your existing "parserOptions.extraFileExtensions"." +"ESLint was configured to run on \`/other/unknownFileType.unknown\` using \`parserOptions.project\`: /tsconfig.json +The extension for the file (\`.unknown\`) is non-standard. It should be added to your existing \`parserOptions.extraFileExtensions\`." `; exports[`parseAndGenerateServices invalid file error messages "parserOptions.extraFileExtensions" is non-empty the extension matches duplicate extension 1`] = ` -""parserOptions.project" has been set for @typescript-eslint/parser. -The file does not match your project config: ts/notIncluded.ts. -You unnecessarily included the extension ".ts" with the "extraFileExtensions" option. This extension is already handled by the parser by default. -The file must be included in at least one of the projects provided." +"ESLint was configured to run on \`/ts/notIncluded.ts\` using \`parserOptions.project\`: /tsconfig.json +You unnecessarily included the extension \`.ts\` with the \`parserOptions.extraFileExtensions\` option. This extension is already handled by the parser by default. +However, that TSConfig does not include this file. Either: +- Change ESLint's list of included files to not include this file +- Change that TSConfig to include this file +- Create a new TSConfig that includes this file and include it in your parserOptions.project +See the TypeScript ESLint docs for more info: https://typescript-eslint.io/docs/linting/troubleshooting##i-get-errors-telling-me-eslint-was-configured-to-run--however-that-tsconfig-does-not--none-of-those-tsconfigs-include-this-file" `; exports[`parseAndGenerateServices invalid file error messages "parserOptions.extraFileExtensions" is non-empty the extension matches the file isn't included 1`] = ` -""parserOptions.project" has been set for @typescript-eslint/parser. -The file does not match your project config: other/notIncluded.vue. -The file must be included in at least one of the projects provided." +"ESLint was configured to run on \`/other/notIncluded.vue\` using \`parserOptions.project\`: /tsconfig.json +However, that TSConfig does not include this file. Either: +- Change ESLint's list of included files to not include this file +- Change that TSConfig to include this file +- Create a new TSConfig that includes this file and include it in your parserOptions.project +See the TypeScript ESLint docs for more info: https://typescript-eslint.io/docs/linting/troubleshooting##i-get-errors-telling-me-eslint-was-configured-to-run--however-that-tsconfig-does-not--none-of-those-tsconfigs-include-this-file" `; exports[`parseAndGenerateServices invalid file error messages project includes errors for not included files 1`] = ` -""parserOptions.project" has been set for @typescript-eslint/parser. -The file does not match your project config: ts/notIncluded0j1.ts. -The file must be included in at least one of the projects provided." +"ESLint was configured to run on \`/ts/notIncluded0j1.ts\` using \`parserOptions.project\`: /tsconfig.json +However, that TSConfig does not include this file. Either: +- Change ESLint's list of included files to not include this file +- Change that TSConfig to include this file +- Create a new TSConfig that includes this file and include it in your parserOptions.project +See the TypeScript ESLint docs for more info: https://typescript-eslint.io/docs/linting/troubleshooting##i-get-errors-telling-me-eslint-was-configured-to-run--however-that-tsconfig-does-not--none-of-those-tsconfigs-include-this-file" `; exports[`parseAndGenerateServices invalid file error messages project includes errors for not included files 2`] = ` -""parserOptions.project" has been set for @typescript-eslint/parser. -The file does not match your project config: ts/notIncluded02.tsx. -The file must be included in at least one of the projects provided." +"ESLint was configured to run on \`/ts/notIncluded02.tsx\` using \`parserOptions.project\`: /tsconfig.json +However, that TSConfig does not include this file. Either: +- Change ESLint's list of included files to not include this file +- Change that TSConfig to include this file +- Create a new TSConfig that includes this file and include it in your parserOptions.project +See the TypeScript ESLint docs for more info: https://typescript-eslint.io/docs/linting/troubleshooting##i-get-errors-telling-me-eslint-was-configured-to-run--however-that-tsconfig-does-not--none-of-those-tsconfigs-include-this-file" `; exports[`parseAndGenerateServices invalid file error messages project includes errors for not included files 3`] = ` -""parserOptions.project" has been set for @typescript-eslint/parser. -The file does not match your project config: js/notIncluded01.js. -The file must be included in at least one of the projects provided." +"ESLint was configured to run on \`/js/notIncluded01.js\` using \`parserOptions.project\`: /tsconfig.json +However, that TSConfig does not include this file. Either: +- Change ESLint's list of included files to not include this file +- Change that TSConfig to include this file +- Create a new TSConfig that includes this file and include it in your parserOptions.project +See the TypeScript ESLint docs for more info: https://typescript-eslint.io/docs/linting/troubleshooting##i-get-errors-telling-me-eslint-was-configured-to-run--however-that-tsconfig-does-not--none-of-those-tsconfigs-include-this-file" `; exports[`parseAndGenerateServices invalid file error messages project includes errors for not included files 4`] = ` -""parserOptions.project" has been set for @typescript-eslint/parser. -The file does not match your project config: js/notIncluded02.jsx. -The file must be included in at least one of the projects provided." +"ESLint was configured to run on \`/js/notIncluded02.jsx\` using \`parserOptions.project\`: /tsconfig.json +However, that TSConfig does not include this file. Either: +- Change ESLint's list of included files to not include this file +- Change that TSConfig to include this file +- Create a new TSConfig that includes this file and include it in your parserOptions.project +See the TypeScript ESLint docs for more info: https://typescript-eslint.io/docs/linting/troubleshooting##i-get-errors-telling-me-eslint-was-configured-to-run--however-that-tsconfig-does-not--none-of-those-tsconfigs-include-this-file" +`; + +exports[`parseAndGenerateServices invalid project error messages throws when non of multiple projects include the file 1`] = ` +"ESLint was configured to run on \`/ts/notIncluded0j1.ts\` using \`parserOptions.project\`: +- /tsconfig.json +- /tsconfig.extra.json +However, none of those TSConfigs include this file. Either: +- Change ESLint's list of included files to not include this file +- Change one of those TSConfigs to include this file +- Create a new TSConfig that includes this file and include it in your parserOptions.project +See the TypeScript ESLint docs for more info: https://typescript-eslint.io/docs/linting/troubleshooting##i-get-errors-telling-me-eslint-was-configured-to-run--however-that-tsconfig-does-not--none-of-those-tsconfigs-include-this-file" `; exports[`parseAndGenerateServices isolated parsing should parse .js file - with JSX content - parserOptions.jsx = false 1`] = ` diff --git a/packages/typescript-estree/tests/lib/parse.test.ts b/packages/typescript-estree/tests/lib/parse.test.ts index 54d4c12dacd2..dab87e38ab6c 100644 --- a/packages/typescript-estree/tests/lib/parse.test.ts +++ b/packages/typescript-estree/tests/lib/parse.test.ts @@ -35,11 +35,32 @@ jest.mock('../../src/create-program/shared', () => { }; }); +// Tests in CI by default run with lowercase program file names, +// resulting in path.relative results starting with many "../"s +jest.mock('typescript', () => { + const ts = jest.requireActual('typescript'); + return { + ...ts, + sys: { + ...ts.sys, + useCaseSensitiveFileNames: true, + }, + }; +}); + const astConverterMock = jest.mocked(astConverterModule.astConverter); const createDefaultCompilerOptionsFromExtra = jest.mocked( sharedParserUtilsModule.createDefaultCompilerOptionsFromExtra, ); +/** + * Aligns paths between environments, node for windows uses `\`, for linux and mac uses `/` + */ +function alignErrorPath(error: Error): never { + error.message = error.message.replace(/\\(?!["])/gm, '/'); + throw error; +} + beforeEach(() => { jest.clearAllMocks(); }); @@ -539,14 +560,7 @@ describe('parseAndGenerateServices', () => { filePath: join(PROJECT_DIR, filePath), }); } catch (error) { - /** - * Aligns paths between environments, node for windows uses `\`, for linux and mac uses `/` - */ - (error as Error).message = (error as Error).message.replace( - /\\(?!["])/gm, - '/', - ); - throw error; + throw alignErrorPath(error as Error); } }; @@ -617,6 +631,33 @@ describe('parseAndGenerateServices', () => { }); }); + describe('invalid project error messages', () => { + it('throws when non of multiple projects include the file', () => { + const PROJECT_DIR = resolve(FIXTURES_DIR, '../invalidFileErrors'); + const code = 'var a = true'; + const config: TSESTreeOptions = { + comment: true, + tokens: true, + range: true, + loc: true, + tsconfigRootDir: PROJECT_DIR, + project: ['./**/tsconfig.json', './**/tsconfig.extra.json'], + }; + const testParse = (filePath: string) => (): void => { + try { + parser.parseAndGenerateServices(code, { + ...config, + filePath: join(PROJECT_DIR, filePath), + }); + } catch (error) { + throw alignErrorPath(error as Error); + } + }; + + expect(testParse('ts/notIncluded0j1.ts')).toThrowErrorMatchingSnapshot(); + }); + }); + describe('debug options', () => { const debugEnable = jest.fn(); beforeEach(() => { diff --git a/packages/typescript-estree/tests/lib/semanticInfo.test.ts b/packages/typescript-estree/tests/lib/semanticInfo.test.ts index 59a59e08a53a..fae6a3836628 100644 --- a/packages/typescript-estree/tests/lib/semanticInfo.test.ts +++ b/packages/typescript-estree/tests/lib/semanticInfo.test.ts @@ -240,7 +240,9 @@ describe('semanticInfo', () => { `function M() { return Base }`, createOptions(''), ), - ).toThrow(/The file does not match your project config: estree.ts/); + ).toThrow( + /ESLint was configured to run on `\/estree\.ts` using/, + ); }); it('non-existent project file', () => { From c4e0d8678e0398f3ab85510f40ad6f97832b9e6d Mon Sep 17 00:00:00 2001 From: Sviatoslav Miller Date: Tue, 25 Oct 2022 16:51:35 +0300 Subject: [PATCH 04/20] feat(website): Add a happy message to playground output pane when no errors or AST (#5868) (#5873) * feat(website): Add a happy message to playground output pane when no errors or AST (#5868) * Apply suggestions from code review Co-authored-by: Josh Goldberg --- .../website/src/components/ErrorsViewer.tsx | 68 ++++++++++++------- 1 file changed, 43 insertions(+), 25 deletions(-) diff --git a/packages/website/src/components/ErrorsViewer.tsx b/packages/website/src/components/ErrorsViewer.tsx index 3e0a890ee0a6..0789b84d78c0 100644 --- a/packages/website/src/components/ErrorsViewer.tsx +++ b/packages/website/src/components/ErrorsViewer.tsx @@ -91,6 +91,18 @@ function ErrorBlock({ ); } +function SuccessBlock(): JSX.Element { + return ( +
+
+
+
All is ok!
+
+
+
+ ); +} + export default function ErrorsViewer({ value, }: ErrorsViewerProps): JSX.Element { @@ -113,31 +125,37 @@ export default function ErrorsViewer({ return (
- {value?.map(({ group, uri, items }) => { - return ( -
-

- {group} - {uri && ( - <> - {' - '} - - docs - - - )} -

- {items.map((item, index) => ( - - ))} -
- ); - })} + {value?.length ? ( + value.map(({ group, uri, items }) => { + return ( +
+

+ {group} + {uri && ( + <> + {' - '} + + docs + + + )} +

+ {items.map((item, index) => ( + + ))} +
+ ); + }) + ) : ( +
+ +
+ )}
); } From 8ddcc9a767a0794b17e603a8b11af998154ad3cb Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Tue, 25 Oct 2022 18:22:28 -0400 Subject: [PATCH 05/20] chore: document and refactor 'extra' to 'parserSettings' (#5834) --- .../typescript-estree/src/ast-converter.ts | 18 +- .../create-program/createDefaultProgram.ts | 35 +- .../create-program/createIsolatedProgram.ts | 22 +- .../create-program/createProjectProgram.ts | 38 +- .../src/create-program/createSourceFile.ts | 14 +- .../src/create-program/createWatchProgram.ts | 62 +-- .../src/create-program/shared.ts | 17 +- .../src/create-program/useProvidedPrograms.ts | 12 +- .../src/parseSettings/createParseSettings.ts | 201 ++++++++ .../src/parseSettings/index.ts | 124 +++++ .../src/parseSettings/inferSingleRun.ts | 46 ++ .../src/parseSettings/warnAboutTSVersion.ts | 45 ++ .../typescript-estree/src/parser-options.ts | 28 - packages/typescript-estree/src/parser.ts | 487 ++---------------- .../src/components/linter/WebLinter.ts | 4 +- .../website/src/components/linter/config.ts | 7 +- 16 files changed, 572 insertions(+), 588 deletions(-) create mode 100644 packages/typescript-estree/src/parseSettings/createParseSettings.ts create mode 100644 packages/typescript-estree/src/parseSettings/index.ts create mode 100644 packages/typescript-estree/src/parseSettings/inferSingleRun.ts create mode 100644 packages/typescript-estree/src/parseSettings/warnAboutTSVersion.ts diff --git a/packages/typescript-estree/src/ast-converter.ts b/packages/typescript-estree/src/ast-converter.ts index 86a1970aef79..b9be864f5298 100644 --- a/packages/typescript-estree/src/ast-converter.ts +++ b/packages/typescript-estree/src/ast-converter.ts @@ -4,13 +4,13 @@ import type { ASTMaps } from './convert'; import { Converter, convertError } from './convert'; import { convertComments } from './convert-comments'; import { convertTokens } from './node-utils'; -import type { Extra } from './parser-options'; +import type { ParseSettings } from './parseSettings'; import { simpleTraverse } from './simple-traverse'; import type { TSESTree } from './ts-estree'; export function astConverter( ast: SourceFile, - extra: Extra, + parseSettings: ParseSettings, shouldPreserveNodeMaps: boolean, ): { estree: TSESTree.Program; astMaps: ASTMaps } { /** @@ -26,7 +26,7 @@ export function astConverter( * Recursively convert the TypeScript AST into an ESTree-compatible AST */ const instance = new Converter(ast, { - errorOnUnknownASTType: extra.errorOnUnknownASTType || false, + errorOnUnknownASTType: parseSettings.errorOnUnknownASTType || false, shouldPreserveNodeMaps, }); @@ -35,15 +35,15 @@ export function astConverter( /** * Optionally remove range and loc if specified */ - if (!extra.range || !extra.loc) { + if (!parseSettings.range || !parseSettings.loc) { simpleTraverse(estree, { enter: node => { - if (!extra.range) { + if (!parseSettings.range) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment -- TS 4.0 made this an error because the types aren't optional // @ts-expect-error delete node.range; } - if (!extra.loc) { + if (!parseSettings.loc) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment -- TS 4.0 made this an error because the types aren't optional // @ts-expect-error delete node.loc; @@ -55,15 +55,15 @@ export function astConverter( /** * Optionally convert and include all tokens in the AST */ - if (extra.tokens) { + if (parseSettings.tokens) { estree.tokens = convertTokens(ast); } /** * Optionally convert and include all comments in the AST */ - if (extra.comment) { - estree.comments = convertComments(ast, extra.code); + if (parseSettings.comment) { + estree.comments = convertComments(ast, parseSettings.code); } const astMaps = instance.getASTMaps(); diff --git a/packages/typescript-estree/src/create-program/createDefaultProgram.ts b/packages/typescript-estree/src/create-program/createDefaultProgram.ts index bebb194aef5e..a2de81399d20 100644 --- a/packages/typescript-estree/src/create-program/createDefaultProgram.ts +++ b/packages/typescript-estree/src/create-program/createDefaultProgram.ts @@ -2,8 +2,8 @@ import debug from 'debug'; import path from 'path'; import * as ts from 'typescript'; -import type { Extra } from '../parser-options'; -import type { ASTAndProgram, CanonicalPath } from './shared'; +import type { ParseSettings } from '../parseSettings'; +import type { ASTAndProgram } from './shared'; import { createDefaultCompilerOptionsFromExtra, getModuleResolver, @@ -12,27 +12,26 @@ import { const log = debug('typescript-eslint:typescript-estree:createDefaultProgram'); /** - * @param code The code of the file being linted - * @param extra The config object - * @param extra.tsconfigRootDir The root directory for relative tsconfig paths - * @param extra.projects Provided tsconfig paths + * @param parseSettings Internal settings for parsing the file * @returns If found, returns the source file corresponding to the code and the containing program */ function createDefaultProgram( - code: string, - extra: Extra, + parseSettings: ParseSettings, ): ASTAndProgram | undefined { - log('Getting default program for: %s', extra.filePath || 'unnamed file'); + log( + 'Getting default program for: %s', + parseSettings.filePath || 'unnamed file', + ); - if (!extra.projects || extra.projects.length !== 1) { + if (parseSettings.projects?.length !== 1) { return undefined; } - const tsconfigPath: CanonicalPath = extra.projects[0]; + const tsconfigPath = parseSettings.projects[0]; const commandLine = ts.getParsedCommandLineOfConfigFile( tsconfigPath, - createDefaultCompilerOptionsFromExtra(extra), + createDefaultCompilerOptionsFromExtra(parseSettings), { ...ts.sys, onUnRecoverableConfigFileDiagnostic: () => {} }, ); @@ -45,24 +44,24 @@ function createDefaultProgram( /* setParentNodes */ true, ); - if (extra.moduleResolver) { + if (parseSettings.moduleResolver) { compilerHost.resolveModuleNames = getModuleResolver( - extra.moduleResolver, + parseSettings.moduleResolver, ).resolveModuleNames; } const oldReadFile = compilerHost.readFile; compilerHost.readFile = (fileName: string): string | undefined => - path.normalize(fileName) === path.normalize(extra.filePath) - ? code + path.normalize(fileName) === path.normalize(parseSettings.filePath) + ? parseSettings.code : oldReadFile(fileName); const program = ts.createProgram( - [extra.filePath], + [parseSettings.filePath], commandLine.options, compilerHost, ); - const ast = program.getSourceFile(extra.filePath); + const ast = program.getSourceFile(parseSettings.filePath); return ast && { ast, program }; } diff --git a/packages/typescript-estree/src/create-program/createIsolatedProgram.ts b/packages/typescript-estree/src/create-program/createIsolatedProgram.ts index ba19b843aeb6..5ec1c8e0fe75 100644 --- a/packages/typescript-estree/src/create-program/createIsolatedProgram.ts +++ b/packages/typescript-estree/src/create-program/createIsolatedProgram.ts @@ -1,7 +1,7 @@ import debug from 'debug'; import * as ts from 'typescript'; -import type { Extra } from '../parser-options'; +import type { ParseSettings } from '../parseSettings'; import { getScriptKind } from './getScriptKind'; import type { ASTAndProgram } from './shared'; import { createDefaultCompilerOptionsFromExtra } from './shared'; @@ -12,11 +12,11 @@ const log = debug('typescript-eslint:typescript-estree:createIsolatedProgram'); * @param code The code of the file being linted * @returns Returns a new source file and program corresponding to the linted code */ -function createIsolatedProgram(code: string, extra: Extra): ASTAndProgram { +function createIsolatedProgram(parseSettings: ParseSettings): ASTAndProgram { log( 'Getting isolated program in %s mode for: %s', - extra.jsx ? 'TSX' : 'TS', - extra.filePath, + parseSettings.jsx ? 'TSX' : 'TS', + parseSettings.filePath, ); const compilerHost: ts.CompilerHost = { @@ -24,7 +24,7 @@ function createIsolatedProgram(code: string, extra: Extra): ASTAndProgram { return true; }, getCanonicalFileName() { - return extra.filePath; + return parseSettings.filePath; }, getCurrentDirectory() { return ''; @@ -43,10 +43,10 @@ function createIsolatedProgram(code: string, extra: Extra): ASTAndProgram { getSourceFile(filename: string) { return ts.createSourceFile( filename, - code, + parseSettings.code, ts.ScriptTarget.Latest, /* setParentNodes */ true, - getScriptKind(extra.filePath, extra.jsx), + getScriptKind(parseSettings.filePath, parseSettings.jsx), ); }, readFile() { @@ -61,17 +61,17 @@ function createIsolatedProgram(code: string, extra: Extra): ASTAndProgram { }; const program = ts.createProgram( - [extra.filePath], + [parseSettings.filePath], { noResolve: true, target: ts.ScriptTarget.Latest, - jsx: extra.jsx ? ts.JsxEmit.Preserve : undefined, - ...createDefaultCompilerOptionsFromExtra(extra), + jsx: parseSettings.jsx ? ts.JsxEmit.Preserve : undefined, + ...createDefaultCompilerOptionsFromExtra(parseSettings), }, compilerHost, ); - const ast = program.getSourceFile(extra.filePath); + const ast = program.getSourceFile(parseSettings.filePath); if (!ast) { throw new Error( 'Expected an ast to be returned for the single-file isolated program.', diff --git a/packages/typescript-estree/src/create-program/createProjectProgram.ts b/packages/typescript-estree/src/create-program/createProjectProgram.ts index 9ece5e4a8273..326100d35784 100644 --- a/packages/typescript-estree/src/create-program/createProjectProgram.ts +++ b/packages/typescript-estree/src/create-program/createProjectProgram.ts @@ -3,7 +3,7 @@ import path from 'path'; import * as ts from 'typescript'; import { firstDefined } from '../node-utils'; -import type { Extra } from '../parser-options'; +import type { ParseSettings } from '../parseSettings'; import { getProgramsForProjects } from './createWatchProgram'; import type { ASTAndProgram } from './shared'; import { getAstFromProgram } from './shared'; @@ -22,45 +22,37 @@ const DEFAULT_EXTRA_FILE_EXTENSIONS = [ ] as readonly string[]; /** - * @param code The code of the file being linted - * @param createDefaultProgram True if the default program should be created - * @param extra The config object - * @returns If found, returns the source file corresponding to the code and the containing program + * @param parseSettings Internal settings for parsing the file + * @returns If found, the source file corresponding to the code and the containing program */ function createProjectProgram( - code: string, - createDefaultProgram: boolean, - extra: Extra, + parseSettings: ParseSettings, ): ASTAndProgram | undefined { - log('Creating project program for: %s', extra.filePath); + log('Creating project program for: %s', parseSettings.filePath); - const programsForProjects = getProgramsForProjects( - code, - extra.filePath, - extra, - ); + const programsForProjects = getProgramsForProjects(parseSettings); const astAndProgram = firstDefined(programsForProjects, currentProgram => - getAstFromProgram(currentProgram, extra), + getAstFromProgram(currentProgram, parseSettings), ); // The file was either matched within the tsconfig, or we allow creating a default program - if (astAndProgram || createDefaultProgram) { + if (astAndProgram || parseSettings.createDefaultProgram) { return astAndProgram; } const describeFilePath = (filePath: string): string => { const relative = path.relative( - extra.tsconfigRootDir || process.cwd(), + parseSettings.tsconfigRootDir || process.cwd(), filePath, ); - if (extra.tsconfigRootDir) { + if (parseSettings.tsconfigRootDir) { return `/${relative}`; } return `/${relative}`; }; - const describedFilePath = describeFilePath(extra.filePath); - const relativeProjects = extra.projects.map(describeFilePath); + const describedFilePath = describeFilePath(parseSettings.filePath); + const relativeProjects = parseSettings.projects.map(describeFilePath); const describedPrograms = relativeProjects.length === 1 ? relativeProjects[0] @@ -70,7 +62,7 @@ function createProjectProgram( ]; let hasMatchedAnError = false; - const extraFileExtensions = extra.extraFileExtensions || []; + const extraFileExtensions = parseSettings.extraFileExtensions || []; extraFileExtensions.forEach(extraExtension => { if (!extraExtension.startsWith('.')) { @@ -85,7 +77,7 @@ function createProjectProgram( } }); - const fileExtension = path.extname(extra.filePath); + const fileExtension = path.extname(parseSettings.filePath); if (!DEFAULT_EXTRA_FILE_EXTENSIONS.includes(fileExtension)) { const nonStandardExt = `The extension for the file (\`${fileExtension}\`) is non-standard`; if (extraFileExtensions.length > 0) { @@ -105,7 +97,7 @@ function createProjectProgram( if (!hasMatchedAnError) { const [describedInclusions, describedSpecifiers] = - extra.projects.length === 1 + parseSettings.projects.length === 1 ? ['that TSConfig does not', 'that TSConfig'] : ['none of those TSConfigs', 'one of those TSConfigs']; errorLines.push( diff --git a/packages/typescript-estree/src/create-program/createSourceFile.ts b/packages/typescript-estree/src/create-program/createSourceFile.ts index 107f027e46df..806e503f0e42 100644 --- a/packages/typescript-estree/src/create-program/createSourceFile.ts +++ b/packages/typescript-estree/src/create-program/createSourceFile.ts @@ -1,24 +1,24 @@ import debug from 'debug'; import * as ts from 'typescript'; -import type { Extra } from '../parser-options'; +import type { ParseSettings } from '../parseSettings'; import { getScriptKind } from './getScriptKind'; const log = debug('typescript-eslint:typescript-estree:createSourceFile'); -function createSourceFile(code: string, extra: Extra): ts.SourceFile { +function createSourceFile(parseSettings: ParseSettings): ts.SourceFile { log( 'Getting AST without type information in %s mode for: %s', - extra.jsx ? 'TSX' : 'TS', - extra.filePath, + parseSettings.jsx ? 'TSX' : 'TS', + parseSettings.filePath, ); return ts.createSourceFile( - extra.filePath, - code, + parseSettings.filePath, + parseSettings.code, ts.ScriptTarget.Latest, /* setParentNodes */ true, - getScriptKind(extra.filePath, extra.jsx), + getScriptKind(parseSettings.filePath, parseSettings.jsx), ); } diff --git a/packages/typescript-estree/src/create-program/createWatchProgram.ts b/packages/typescript-estree/src/create-program/createWatchProgram.ts index 1511023a97e8..0e32f8ec1e58 100644 --- a/packages/typescript-estree/src/create-program/createWatchProgram.ts +++ b/packages/typescript-estree/src/create-program/createWatchProgram.ts @@ -3,7 +3,7 @@ import fs from 'fs'; import semver from 'semver'; import * as ts from 'typescript'; -import type { Extra } from '../parser-options'; +import type { ParseSettings } from '../parseSettings'; import type { CanonicalPath } from './shared'; import { canonicalDirname, @@ -121,40 +121,34 @@ function createHash(content: string): string { function updateCachedFileList( tsconfigPath: CanonicalPath, program: ts.Program, - extra: Extra, + parseSettings: ParseSettings, ): Set { - const fileList = extra.EXPERIMENTAL_useSourceOfProjectReferenceRedirect - ? new Set( - program.getSourceFiles().map(sf => getCanonicalFileName(sf.fileName)), - ) - : new Set(program.getRootFileNames().map(f => getCanonicalFileName(f))); + const fileList = + parseSettings.EXPERIMENTAL_useSourceOfProjectReferenceRedirect + ? new Set( + program.getSourceFiles().map(sf => getCanonicalFileName(sf.fileName)), + ) + : new Set(program.getRootFileNames().map(f => getCanonicalFileName(f))); programFileListCache.set(tsconfigPath, fileList); return fileList; } /** * Calculate project environments using options provided by consumer and paths from config - * @param code The code being linted - * @param filePathIn The path of the file being parsed - * @param extra.tsconfigRootDir The root directory for relative tsconfig paths - * @param extra.projects Provided tsconfig paths + * @param parseSettings Internal settings for parsing the file * @returns The programs corresponding to the supplied tsconfig paths */ -function getProgramsForProjects( - code: string, - filePathIn: string, - extra: Extra, -): ts.Program[] { - const filePath = getCanonicalFileName(filePathIn); +function getProgramsForProjects(parseSettings: ParseSettings): ts.Program[] { + const filePath = getCanonicalFileName(parseSettings.filePath); const results = []; // preserve reference to code and file being linted - currentLintOperationState.code = code; + currentLintOperationState.code = parseSettings.code; currentLintOperationState.filePath = filePath; // Update file version if necessary const fileWatchCallbacks = fileWatchCallbackTrackingMap.get(filePath); - const codeHash = createHash(code); + const codeHash = createHash(parseSettings.code); if ( parsedFilesSeenHash.get(filePath) !== codeHash && fileWatchCallbacks && @@ -174,7 +168,11 @@ function getProgramsForProjects( let updatedProgram: ts.Program | null = null; if (!fileList) { updatedProgram = existingWatch.getProgram().getProgram(); - fileList = updateCachedFileList(tsconfigPath, updatedProgram, extra); + fileList = updateCachedFileList( + tsconfigPath, + updatedProgram, + parseSettings, + ); } if (fileList.has(filePath)) { @@ -198,7 +196,7 @@ function getProgramsForProjects( * - the required program hasn't been created yet, or * - the file is new/renamed, and the program hasn't been updated. */ - for (const tsconfigPath of extra.projects) { + for (const tsconfigPath of parseSettings.projects) { const existingWatch = knownWatchProgramMap.get(tsconfigPath); if (existingWatch) { @@ -218,7 +216,7 @@ function getProgramsForProjects( const fileList = updateCachedFileList( tsconfigPath, updatedProgram, - extra, + parseSettings, ); if (fileList.has(filePath)) { log('Found updated program for file. %s', filePath); @@ -230,7 +228,7 @@ function getProgramsForProjects( continue; } - const programWatch = createWatchProgram(tsconfigPath, extra); + const programWatch = createWatchProgram(tsconfigPath, parseSettings); knownWatchProgramMap.set(tsconfigPath, programWatch); const program = programWatch.getProgram().getProgram(); @@ -238,7 +236,7 @@ function getProgramsForProjects( program.getTypeChecker(); // cache and check the file list - const fileList = updateCachedFileList(tsconfigPath, program, extra); + const fileList = updateCachedFileList(tsconfigPath, program, parseSettings); if (fileList.has(filePath)) { log('Found program for file. %s', filePath); // we can return early because we know this program contains the file @@ -257,23 +255,23 @@ const isRunningNoTimeoutFix = semver.satisfies(ts.version, '>=3.9.0-beta', { function createWatchProgram( tsconfigPath: string, - extra: Extra, + parseSettings: ParseSettings, ): ts.WatchOfConfigFile { log('Creating watch program for %s.', tsconfigPath); // create compiler host const watchCompilerHost = ts.createWatchCompilerHost( tsconfigPath, - createDefaultCompilerOptionsFromExtra(extra), + createDefaultCompilerOptionsFromExtra(parseSettings), ts.sys, ts.createAbstractBuilder, diagnosticReporter, /*reportWatchStatus*/ () => {}, ) as WatchCompilerHostOfConfigFile; - if (extra.moduleResolver) { + if (parseSettings.moduleResolver) { watchCompilerHost.resolveModuleNames = getModuleResolver( - extra.moduleResolver, + parseSettings.moduleResolver, ).resolveModuleNames; } @@ -337,7 +335,9 @@ function createWatchProgram( ): string[] => oldReadDirectory( path, - !extensions ? undefined : extensions.concat(extra.extraFileExtensions), + !extensions + ? undefined + : extensions.concat(parseSettings.extraFileExtensions), exclude, include, depth, @@ -345,7 +345,7 @@ function createWatchProgram( oldOnDirectoryStructureHostCreate(host); }; // This works only on 3.9 - watchCompilerHost.extraFileExtensions = extra.extraFileExtensions.map( + watchCompilerHost.extraFileExtensions = parseSettings.extraFileExtensions.map( extension => ({ extension, isMixedContent: true, @@ -359,7 +359,7 @@ function createWatchProgram( * See https://github.com/typescript-eslint/typescript-eslint/issues/2094 */ watchCompilerHost.useSourceOfProjectReferenceRedirect = (): boolean => - extra.EXPERIMENTAL_useSourceOfProjectReferenceRedirect; + parseSettings.EXPERIMENTAL_useSourceOfProjectReferenceRedirect; // Since we don't want to asynchronously update program we want to disable timeout methods // So any changes in the program will be delayed and updated when getProgram is called on watch diff --git a/packages/typescript-estree/src/create-program/shared.ts b/packages/typescript-estree/src/create-program/shared.ts index 63ba34a5f837..dd50f757dce1 100644 --- a/packages/typescript-estree/src/create-program/shared.ts +++ b/packages/typescript-estree/src/create-program/shared.ts @@ -2,7 +2,8 @@ import path from 'path'; import type { Program } from 'typescript'; import * as ts from 'typescript'; -import type { Extra, ModuleResolver } from '../parser-options'; +import type { ModuleResolver } from '../parser-options'; +import type { ParseSettings } from '../parseSettings'; interface ASTAndProgram { ast: ts.SourceFile; @@ -33,9 +34,9 @@ const DEFAULT_COMPILER_OPTIONS: ts.CompilerOptions = { }; function createDefaultCompilerOptionsFromExtra( - extra: Extra, + parseSettings: ParseSettings, ): ts.CompilerOptions { - if (extra.debugLevel.has('typescript')) { + if (parseSettings.debugLevel.has('typescript')) { return { ...DEFAULT_COMPILER_OPTIONS, extendedDiagnostics: true, @@ -63,10 +64,10 @@ function getCanonicalFileName(filePath: string): CanonicalPath { return correctPathCasing(normalized) as CanonicalPath; } -function ensureAbsolutePath(p: string, extra: Extra): string { +function ensureAbsolutePath(p: string, tsconfigRootDir: string): string { return path.isAbsolute(p) ? p - : path.join(extra.tsconfigRootDir || process.cwd(), p); + : path.join(tsconfigRootDir || process.cwd(), p); } function canonicalDirname(p: CanonicalPath): CanonicalPath { @@ -92,12 +93,12 @@ function getExtension(fileName: string | undefined): string | null { function getAstFromProgram( currentProgram: Program, - extra: Extra, + parseSettings: ParseSettings, ): ASTAndProgram | undefined { - const ast = currentProgram.getSourceFile(extra.filePath); + const ast = currentProgram.getSourceFile(parseSettings.filePath); // working around https://github.com/typescript-eslint/typescript-eslint/issues/1573 - const expectedExt = getExtension(extra.filePath); + const expectedExt = getExtension(parseSettings.filePath); const returnedExt = getExtension(ast?.fileName); if (expectedExt !== returnedExt) { return undefined; diff --git a/packages/typescript-estree/src/create-program/useProvidedPrograms.ts b/packages/typescript-estree/src/create-program/useProvidedPrograms.ts index 0a8300f5ad28..fc99416faa5c 100644 --- a/packages/typescript-estree/src/create-program/useProvidedPrograms.ts +++ b/packages/typescript-estree/src/create-program/useProvidedPrograms.ts @@ -3,7 +3,7 @@ import * as fs from 'fs'; import * as path from 'path'; import * as ts from 'typescript'; -import type { Extra } from '../parser-options'; +import type { ParseSettings } from '../parseSettings'; import type { ASTAndProgram } from './shared'; import { CORE_COMPILER_OPTIONS, getAstFromProgram } from './shared'; @@ -11,16 +11,16 @@ const log = debug('typescript-eslint:typescript-estree:useProvidedProgram'); function useProvidedPrograms( programInstances: Iterable, - extra: Extra, + parseSettings: ParseSettings, ): ASTAndProgram | undefined { log( 'Retrieving ast for %s from provided program instance(s)', - extra.filePath, + parseSettings.filePath, ); let astAndProgram: ASTAndProgram | undefined; for (const programInstance of programInstances) { - astAndProgram = getAstFromProgram(programInstance, extra); + astAndProgram = getAstFromProgram(programInstance, parseSettings); // Stop at the first applicable program instance if (astAndProgram) { break; @@ -29,8 +29,8 @@ function useProvidedPrograms( if (!astAndProgram) { const relativeFilePath = path.relative( - extra.tsconfigRootDir || process.cwd(), - extra.filePath, + parseSettings.tsconfigRootDir || process.cwd(), + parseSettings.filePath, ); const errorLines = [ '"parserOptions.programs" has been provided for @typescript-eslint/parser.', diff --git a/packages/typescript-estree/src/parseSettings/createParseSettings.ts b/packages/typescript-estree/src/parseSettings/createParseSettings.ts new file mode 100644 index 000000000000..b1cde9d4c9ad --- /dev/null +++ b/packages/typescript-estree/src/parseSettings/createParseSettings.ts @@ -0,0 +1,201 @@ +import debug from 'debug'; +import { sync as globSync } from 'globby'; +import isGlob from 'is-glob'; + +import type { CanonicalPath } from '../create-program/shared'; +import { + ensureAbsolutePath, + getCanonicalFileName, +} from '../create-program/shared'; +import type { TSESTreeOptions } from '../parser-options'; +import type { MutableParseSettings } from './index'; +import { inferSingleRun } from './inferSingleRun'; +import { warnAboutTSVersion } from './warnAboutTSVersion'; + +const log = debug( + 'typescript-eslint:typescript-estree:parser:parseSettings:createParseSettings', +); + +export function createParseSettings( + code: string, + options: Partial = {}, +): MutableParseSettings { + const tsconfigRootDir = + typeof options.tsconfigRootDir === 'string' + ? options.tsconfigRootDir + : process.cwd(); + const parseSettings: MutableParseSettings = { + code: enforceString(code), + comment: options.comment === true, + comments: [], + createDefaultProgram: options.createDefaultProgram === true, + debugLevel: + options.debugLevel === true + ? new Set(['typescript-eslint']) + : Array.isArray(options.debugLevel) + ? new Set(options.debugLevel) + : new Set(), + errorOnTypeScriptSyntacticAndSemanticIssues: false, + errorOnUnknownASTType: options.errorOnUnknownASTType === true, + EXPERIMENTAL_useSourceOfProjectReferenceRedirect: + options.EXPERIMENTAL_useSourceOfProjectReferenceRedirect === true, + extraFileExtensions: + Array.isArray(options.extraFileExtensions) && + options.extraFileExtensions.every(ext => typeof ext === 'string') + ? options.extraFileExtensions + : [], + filePath: ensureAbsolutePath( + typeof options.filePath === 'string' && options.filePath !== '' + ? options.filePath + : getFileName(options.jsx), + tsconfigRootDir, + ), + jsx: options.jsx === true, + loc: options.loc === true, + log: + typeof options.loggerFn === 'function' + ? options.loggerFn + : options.loggerFn === false + ? (): void => {} + : console.log, // eslint-disable-line no-console + moduleResolver: options.moduleResolver ?? '', + preserveNodeMaps: options.preserveNodeMaps !== false, + programs: Array.isArray(options.programs) ? options.programs : null, + projects: [], + range: options.range === true, + singleRun: inferSingleRun(options), + tokens: options.tokens === true ? [] : null, + tsconfigRootDir, + }; + + // debug doesn't support multiple `enable` calls, so have to do it all at once + if (parseSettings.debugLevel.size > 0) { + const namespaces = []; + if (parseSettings.debugLevel.has('typescript-eslint')) { + namespaces.push('typescript-eslint:*'); + } + if ( + parseSettings.debugLevel.has('eslint') || + // make sure we don't turn off the eslint debug if it was enabled via --debug + debug.enabled('eslint:*,-eslint:code-path') + ) { + // https://github.com/eslint/eslint/blob/9dfc8501fb1956c90dc11e6377b4cb38a6bea65d/bin/eslint.js#L25 + namespaces.push('eslint:*,-eslint:code-path'); + } + debug.enable(namespaces.join(',')); + } + + 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.`, + ); + } + log( + 'parserOptions.programs was provided, so parserOptions.project will be ignored.', + ); + } + + // Providing a program overrides project resolution + if (!parseSettings.programs) { + const projectFolderIgnoreList = ( + options.projectFolderIgnoreList ?? ['**/node_modules/**'] + ) + .reduce((acc, folder) => { + if (typeof folder === 'string') { + acc.push(folder); + } + return acc; + }, []) + // prefix with a ! for not match glob + .map(folder => (folder.startsWith('!') ? folder : `!${folder}`)); + + parseSettings.projects = prepareAndTransformProjects( + tsconfigRootDir, + options.project, + projectFolderIgnoreList, + ); + } + + warnAboutTSVersion(parseSettings); + + return parseSettings; +} + +/** + * Ensures source code is a string. + */ +function enforceString(code: unknown): string { + if (typeof code !== 'string') { + return String(code); + } + + return code; +} + +/** + * Compute the filename based on the parser options. + * + * Even if jsx option is set in typescript compiler, filename still has to + * contain .tsx file extension. + * + * @param options Parser options + */ +function getFileName(jsx?: boolean): string { + return jsx ? 'estree.tsx' : 'estree.ts'; +} + +function getTsconfigPath( + tsconfigPath: string, + tsconfigRootDir: string, +): CanonicalPath { + return getCanonicalFileName( + ensureAbsolutePath(tsconfigPath, tsconfigRootDir), + ); +} + +/** + * Normalizes, sanitizes, resolves and filters the provided project paths + */ +function prepareAndTransformProjects( + tsconfigRootDir: string, + projectsInput: string | string[] | undefined, + ignoreListInput: string[], +): CanonicalPath[] { + const sanitizedProjects: string[] = []; + + // Normalize and sanitize the project paths + if (typeof projectsInput === 'string') { + sanitizedProjects.push(projectsInput); + } else if (Array.isArray(projectsInput)) { + for (const project of projectsInput) { + if (typeof project === 'string') { + sanitizedProjects.push(project); + } + } + } + + if (sanitizedProjects.length === 0) { + return []; + } + + // Transform glob patterns into paths + const nonGlobProjects = sanitizedProjects.filter(project => !isGlob(project)); + const globProjects = sanitizedProjects.filter(project => isGlob(project)); + const uniqueCanonicalProjectPaths = new Set( + nonGlobProjects + .concat( + globSync([...globProjects, ...ignoreListInput], { + cwd: tsconfigRootDir, + }), + ) + .map(project => getTsconfigPath(project, tsconfigRootDir)), + ); + + log( + 'parserOptions.project (excluding ignored) matched projects: %s', + uniqueCanonicalProjectPaths, + ); + + return Array.from(uniqueCanonicalProjectPaths); +} diff --git a/packages/typescript-estree/src/parseSettings/index.ts b/packages/typescript-estree/src/parseSettings/index.ts new file mode 100644 index 000000000000..0a9734d1b241 --- /dev/null +++ b/packages/typescript-estree/src/parseSettings/index.ts @@ -0,0 +1,124 @@ +import type * as ts from 'typescript'; + +import type { CanonicalPath } from '../create-program/shared'; +import type { TSESTree } from '../ts-estree'; + +type DebugModule = 'typescript-eslint' | 'eslint' | 'typescript'; + +/** + * Internal settings used by the parser to run on a file. + */ +export interface MutableParseSettings { + /** + * Code of the file being parsed. + */ + code: string; + + /** + * Whether the `comment` parse option is enabled. + */ + comment: boolean; + + /** + * If the `comment` parse option is enabled, retrieved comments. + */ + comments: TSESTree.Comment[]; + + /** + * Whether to create a TypeScript program if one is not provided. + */ + createDefaultProgram: boolean; + + /** + * Which debug areas should be logged. + */ + debugLevel: Set; + + /** + * Whether to error if TypeScript reports a semantic or syntactic error diagnostic. + */ + errorOnTypeScriptSyntacticAndSemanticIssues: boolean; + + /** + * Whether to error if an unknown AST node type is encountered. + */ + errorOnUnknownASTType: boolean; + + /** + * Whether TS should use the source files for referenced projects instead of the compiled .d.ts files. + * + * @remarks + * This feature is not yet optimized, and is likely to cause OOMs for medium to large projects. + * This flag REQUIRES at least TS v3.9, otherwise it does nothing. + */ + EXPERIMENTAL_useSourceOfProjectReferenceRedirect: boolean; + + /** + * Any non-standard file extensions which will be parsed. + */ + extraFileExtensions: string[]; + + /** + * Path of the file being parsed. + */ + filePath: string; + + /** + * Whether parsing of JSX is enabled. + * + * @remarks The applicable file extension is still required. + */ + jsx: boolean; + + /** + * Whether to add `loc` information to each node. + */ + loc: boolean; + + /** + * Log function, if not `console.log`. + */ + log: (message: string) => void; + + /** + * Path for a module resolver to use for the compiler host's `resolveModuleNames`. + */ + moduleResolver: string; + + /** + * Whether two-way AST node maps are preserved during the AST conversion process. + */ + preserveNodeMaps?: boolean; + + /** + * One or more instances of TypeScript Program objects to be used for type information. + */ + programs: null | Iterable; + + /** + * Normalized paths to provided project paths. + */ + projects: CanonicalPath[]; + + /** + * Whether to add the `range` property to AST nodes. + */ + range: boolean; + + /** + * Whether this is part of a single run, rather than a long-running process. + */ + singleRun: boolean; + + /** + * If the `tokens` parse option is enabled, retrieved tokens. + */ + tokens: null | TSESTree.Token[]; + + /** + * The absolute path to the root directory for all provided `project`s. + */ + tsconfigRootDir: string; +} + +export type ParseSettings = Readonly; diff --git a/packages/typescript-estree/src/parseSettings/inferSingleRun.ts b/packages/typescript-estree/src/parseSettings/inferSingleRun.ts new file mode 100644 index 000000000000..723f857ece96 --- /dev/null +++ b/packages/typescript-estree/src/parseSettings/inferSingleRun.ts @@ -0,0 +1,46 @@ +import { normalize } from 'path'; + +import type { TSESTreeOptions } from '../parser-options'; + +/** + * ESLint (and therefore typescript-eslint) is used in both "single run"/one-time contexts, + * such as an ESLint CLI invocation, and long-running sessions (such as continuous feedback + * on a file in an IDE). + * + * When typescript-eslint handles TypeScript Program management behind the scenes, this distinction + * is important because there is significant overhead to managing the so called Watch Programs + * needed for the long-running use-case. We therefore use the following logic to figure out which + * of these contexts applies to the current execution. + * + * @returns Whether this is part of a single run, rather than a long-running process. + */ +export function inferSingleRun(options: TSESTreeOptions | undefined): boolean { + // Allow users to explicitly inform us of their intent to perform a single run (or not) with TSESTREE_SINGLE_RUN + if (process.env.TSESTREE_SINGLE_RUN === 'false') { + return false; + } + if (process.env.TSESTREE_SINGLE_RUN === 'true') { + return true; + } + + // Currently behind a flag while we gather real-world feedback + if (options?.allowAutomaticSingleRunInference) { + if ( + // Default to single runs for CI processes. CI=true is set by most CI providers by default. + process.env.CI === 'true' || + // This will be true for invocations such as `npx eslint ...` and `./node_modules/.bin/eslint ...` + process.argv[1].endsWith(normalize('node_modules/.bin/eslint')) + ) { + return true; + } + } + + /** + * We default to assuming that this run could be part of a long-running session (e.g. in an IDE) + * and watch programs will therefore be required. + * + * Unless we can reliably infer otherwise, we default to assuming that this run could be part + * of a long-running session (e.g. in an IDE) and watch programs will therefore be required + */ + return false; +} diff --git a/packages/typescript-estree/src/parseSettings/warnAboutTSVersion.ts b/packages/typescript-estree/src/parseSettings/warnAboutTSVersion.ts new file mode 100644 index 000000000000..6bd890bdae91 --- /dev/null +++ b/packages/typescript-estree/src/parseSettings/warnAboutTSVersion.ts @@ -0,0 +1,45 @@ +import semver from 'semver'; +import * as ts from 'typescript'; + +import type { ParseSettings } from './index'; +/** + * This needs to be kept in sync with the top-level README.md in the + * typescript-eslint monorepo + */ +const SUPPORTED_TYPESCRIPT_VERSIONS = '>=3.3.1 <4.9.0'; + +/* + * The semver package will ignore prerelease ranges, and we don't want to explicitly document every one + * List them all separately here, so we can automatically create the full string + */ +const SUPPORTED_PRERELEASE_RANGES: string[] = []; +const ACTIVE_TYPESCRIPT_VERSION = ts.version; +const isRunningSupportedTypeScriptVersion = semver.satisfies( + ACTIVE_TYPESCRIPT_VERSION, + [SUPPORTED_TYPESCRIPT_VERSIONS] + .concat(SUPPORTED_PRERELEASE_RANGES) + .join(' || '), +); + +let warnedAboutTSVersion = false; + +export function warnAboutTSVersion(parseSettings: ParseSettings): void { + if (!isRunningSupportedTypeScriptVersion && !warnedAboutTSVersion) { + const isTTY = + typeof process === 'undefined' ? false : process.stdout?.isTTY; + if (isTTY) { + const border = '============='; + const versionWarning = [ + border, + 'WARNING: You are currently running a version of TypeScript which is not officially supported by @typescript-eslint/typescript-estree.', + 'You may find that it works just fine, or you may not.', + `SUPPORTED TYPESCRIPT VERSIONS: ${SUPPORTED_TYPESCRIPT_VERSIONS}`, + `YOUR TYPESCRIPT VERSION: ${ACTIVE_TYPESCRIPT_VERSION}`, + 'Please only submit bug reports when using the officially supported version.', + border, + ]; + parseSettings.log(versionWarning.join('\n\n')); + } + warnedAboutTSVersion = true; + } +} diff --git a/packages/typescript-estree/src/parser-options.ts b/packages/typescript-estree/src/parser-options.ts index cfe82b15f228..cec95c3b413d 100644 --- a/packages/typescript-estree/src/parser-options.ts +++ b/packages/typescript-estree/src/parser-options.ts @@ -1,36 +1,8 @@ import type { DebugLevel } from '@typescript-eslint/types'; import type * as ts from 'typescript'; -import type { CanonicalPath } from './create-program/shared'; import type { TSESTree, TSESTreeToTSNode, TSNode, TSToken } from './ts-estree'; -type DebugModule = 'typescript-eslint' | 'eslint' | 'typescript'; - -export interface Extra { - code: string; - comment: boolean; - comments: TSESTree.Comment[]; - createDefaultProgram: boolean; - debugLevel: Set; - errorOnTypeScriptSyntacticAndSemanticIssues: boolean; - errorOnUnknownASTType: boolean; - EXPERIMENTAL_useSourceOfProjectReferenceRedirect: boolean; - extraFileExtensions: string[]; - filePath: string; - jsx: boolean; - loc: boolean; - singleRun: boolean; - log: (message: string) => void; - preserveNodeMaps?: boolean; - programs: null | Iterable; - projects: CanonicalPath[]; - range: boolean; - strict: boolean; - tokens: null | TSESTree.Token[]; - tsconfigRootDir: string; - moduleResolver: string; -} - //////////////////////////////////////////////////// // MAKE SURE THIS IS KEPT IN SYNC WITH THE README // //////////////////////////////////////////////////// diff --git a/packages/typescript-estree/src/parser.ts b/packages/typescript-estree/src/parser.ts index ae461a24dd5d..c5504ba961a0 100644 --- a/packages/typescript-estree/src/parser.ts +++ b/packages/typescript-estree/src/parser.ts @@ -1,9 +1,5 @@ import debug from 'debug'; -import { sync as globSync } from 'globby'; -import isGlob from 'is-glob'; -import { normalize } from 'path'; -import semver from 'semver'; -import * as ts from 'typescript'; +import type * as ts from 'typescript'; import { astConverter } from './ast-converter'; import { convertError } from './convert'; @@ -12,41 +8,18 @@ import { createIsolatedProgram } from './create-program/createIsolatedProgram'; import { createProjectProgram } from './create-program/createProjectProgram'; import { createSourceFile } from './create-program/createSourceFile'; import type { ASTAndProgram, CanonicalPath } from './create-program/shared'; -import { - ensureAbsolutePath, - getCanonicalFileName, -} from './create-program/shared'; import { createProgramFromConfigFile, useProvidedPrograms, } from './create-program/useProvidedPrograms'; -import type { Extra, ParserServices, TSESTreeOptions } from './parser-options'; +import type { ParserServices, TSESTreeOptions } from './parser-options'; +import type { ParseSettings } from './parseSettings'; +import { createParseSettings } from './parseSettings/createParseSettings'; import { getFirstSemanticOrSyntacticError } from './semantic-or-syntactic-errors'; import type { TSESTree } from './ts-estree'; 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 - */ -const SUPPORTED_TYPESCRIPT_VERSIONS = '>=3.3.1 <4.9.0'; -/* - * The semver package will ignore prerelease ranges, and we don't want to explicitly document every one - * List them all separately here, so we can automatically create the full string - */ -const SUPPORTED_PRERELEASE_RANGES: string[] = []; -const ACTIVE_TYPESCRIPT_VERSION = ts.version; -const isRunningSupportedTypeScriptVersion = semver.satisfies( - ACTIVE_TYPESCRIPT_VERSION, - [SUPPORTED_TYPESCRIPT_VERSIONS] - .concat(SUPPORTED_PRERELEASE_RANGES) - .join(' || '), -); - -let extra: Extra; -let warnedAboutTSVersion = false; - /** * Cache existing programs for the single run use-case. * @@ -57,348 +30,24 @@ function clearProgramCache(): void { existingPrograms.clear(); } -function enforceString(code: unknown): string { - /** - * Ensure the source code is a string - */ - if (typeof code !== 'string') { - return String(code); - } - - return code; -} - /** - * @param code The code of the file being linted - * @param programInstances One or more (potentially lazily constructed) existing programs to use + * @param parseSettings Internal settings for parsing the file * @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, - programInstances: Iterable | null, + parseSettings: ParseSettings, shouldProvideParserServices: boolean, - shouldCreateDefaultProgram: boolean, ): ASTAndProgram { return ( - (programInstances && useProvidedPrograms(programInstances, extra)) || + (parseSettings.programs && + useProvidedPrograms(parseSettings.programs, parseSettings)) || + (shouldProvideParserServices && createProjectProgram(parseSettings)) || (shouldProvideParserServices && - createProjectProgram(code, shouldCreateDefaultProgram, extra)) || - (shouldProvideParserServices && - shouldCreateDefaultProgram && - createDefaultProgram(code, extra)) || - createIsolatedProgram(code, extra) - ); -} - -/** - * Compute the filename based on the parser options. - * - * Even if jsx option is set in typescript compiler, filename still has to - * contain .tsx file extension. - * - * @param options Parser options - */ -function getFileName({ jsx }: { jsx?: boolean } = {}): string { - return jsx ? 'estree.tsx' : 'estree.ts'; -} - -/** - * Resets the extra config object - */ -function resetExtra(): void { - extra = { - code: '', - comment: false, - comments: [], - createDefaultProgram: false, - debugLevel: new Set(), - errorOnTypeScriptSyntacticAndSemanticIssues: false, - errorOnUnknownASTType: false, - EXPERIMENTAL_useSourceOfProjectReferenceRedirect: false, - extraFileExtensions: [], - filePath: getFileName(), - jsx: false, - loc: false, - log: console.log, // eslint-disable-line no-console - preserveNodeMaps: true, - programs: null, - projects: [], - range: false, - strict: false, - tokens: null, - tsconfigRootDir: process.cwd(), - /** - * Unless we can reliably infer otherwise, we default to assuming that this run could be part - * of a long-running session (e.g. in an IDE) and watch programs will therefore be required - */ - singleRun: false, - moduleResolver: '', - }; -} - -function getTsconfigPath(tsconfigPath: string, extra: Extra): CanonicalPath { - return getCanonicalFileName(ensureAbsolutePath(tsconfigPath, extra)); -} - -/** - * Normalizes, sanitizes, resolves and filters the provided project paths - */ -function prepareAndTransformProjects( - projectsInput: string | string[] | undefined, - ignoreListInput: string[], -): CanonicalPath[] { - const sanitizedProjects: string[] = []; - - // Normalize and sanitize the project paths - if (typeof projectsInput === 'string') { - sanitizedProjects.push(projectsInput); - } else if (Array.isArray(projectsInput)) { - for (const project of projectsInput) { - if (typeof project === 'string') { - sanitizedProjects.push(project); - } - } - } - - if (sanitizedProjects.length === 0) { - return []; - } - - // Transform glob patterns into paths - const nonGlobProjects = sanitizedProjects.filter(project => !isGlob(project)); - const globProjects = sanitizedProjects.filter(project => isGlob(project)); - const uniqueCanonicalProjectPaths = new Set( - nonGlobProjects - .concat( - globSync([...globProjects, ...ignoreListInput], { - cwd: extra.tsconfigRootDir, - }), - ) - .map(project => getTsconfigPath(project, extra)), - ); - - log( - 'parserOptions.project (excluding ignored) matched projects: %s', - uniqueCanonicalProjectPaths, + parseSettings.createDefaultProgram && + createDefaultProgram(parseSettings)) || + createIsolatedProgram(parseSettings) ); - - return Array.from(uniqueCanonicalProjectPaths); -} - -function applyParserOptionsToExtra(options: TSESTreeOptions): void { - /** - * Configure Debug logging - */ - if (options.debugLevel === true) { - extra.debugLevel = new Set(['typescript-eslint']); - } else if (Array.isArray(options.debugLevel)) { - extra.debugLevel = new Set(options.debugLevel); - } - if (extra.debugLevel.size > 0) { - // debug doesn't support multiple `enable` calls, so have to do it all at once - const namespaces = []; - if (extra.debugLevel.has('typescript-eslint')) { - namespaces.push('typescript-eslint:*'); - } - if ( - extra.debugLevel.has('eslint') || - // make sure we don't turn off the eslint debug if it was enabled via --debug - debug.enabled('eslint:*,-eslint:code-path') - ) { - // https://github.com/eslint/eslint/blob/9dfc8501fb1956c90dc11e6377b4cb38a6bea65d/bin/eslint.js#L25 - namespaces.push('eslint:*,-eslint:code-path'); - } - debug.enable(namespaces.join(',')); - } - - /** - * Track range information in the AST - */ - extra.range = typeof options.range === 'boolean' && options.range; - extra.loc = typeof options.loc === 'boolean' && options.loc; - - /** - * Track tokens in the AST - */ - if (typeof options.tokens === 'boolean' && options.tokens) { - extra.tokens = []; - } - - /** - * Track comments in the AST - */ - if (typeof options.comment === 'boolean' && options.comment) { - extra.comment = true; - extra.comments = []; - } - - /** - * Enable JSX - note the applicable file extension is still required - */ - if (typeof options.jsx !== 'boolean') { - extra.jsx = false; - } else { - extra.jsx = options.jsx; - } - - /** - * Get the file path - */ - if (typeof options.filePath === 'string' && options.filePath !== '') { - extra.filePath = options.filePath; - } else { - extra.filePath = getFileName(extra); - } - - /** - * Allow the user to cause the parser to error if it encounters an unknown AST Node Type - * (used in testing) - */ - if ( - typeof options.errorOnUnknownASTType === 'boolean' && - options.errorOnUnknownASTType - ) { - extra.errorOnUnknownASTType = true; - } - - /** - * Allow the user to override the function used for logging - */ - if (typeof options.loggerFn === 'function') { - extra.log = options.loggerFn; - } else if (options.loggerFn === false) { - extra.log = (): void => {}; - } - - 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); - - 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.programs was provided, so parserOptions.project will be ignored.', - ); - } - - if (!extra.programs) { - // providing a program overrides project resolution - const projectFolderIgnoreList = ( - options.projectFolderIgnoreList ?? ['**/node_modules/**'] - ) - .reduce((acc, folder) => { - if (typeof folder === 'string') { - acc.push(folder); - } - return acc; - }, []) - // prefix with a ! for not match glob - .map(folder => (folder.startsWith('!') ? folder : `!${folder}`)); - // NOTE - prepareAndTransformProjects relies upon having the correct tsconfigRootDir in extra - extra.projects = prepareAndTransformProjects( - options.project, - projectFolderIgnoreList, - ); - } - - if ( - Array.isArray(options.extraFileExtensions) && - options.extraFileExtensions.every(ext => typeof ext === 'string') - ) { - extra.extraFileExtensions = options.extraFileExtensions; - } - - /** - * Allow the user to enable or disable the preservation of the AST node maps - * during the conversion process. - */ - if (typeof options.preserveNodeMaps === 'boolean') { - extra.preserveNodeMaps = options.preserveNodeMaps; - } - - extra.createDefaultProgram = - typeof options.createDefaultProgram === 'boolean' && - options.createDefaultProgram; - - extra.EXPERIMENTAL_useSourceOfProjectReferenceRedirect = - typeof options.EXPERIMENTAL_useSourceOfProjectReferenceRedirect === - 'boolean' && options.EXPERIMENTAL_useSourceOfProjectReferenceRedirect; - - if (typeof options.moduleResolver === 'string') { - extra.moduleResolver = options.moduleResolver; - } -} - -function warnAboutTSVersion(): void { - if (!isRunningSupportedTypeScriptVersion && !warnedAboutTSVersion) { - const isTTY = - typeof process === 'undefined' ? false : process.stdout?.isTTY; - if (isTTY) { - const border = '============='; - const versionWarning = [ - border, - 'WARNING: You are currently running a version of TypeScript which is not officially supported by @typescript-eslint/typescript-estree.', - 'You may find that it works just fine, or you may not.', - `SUPPORTED TYPESCRIPT VERSIONS: ${SUPPORTED_TYPESCRIPT_VERSIONS}`, - `YOUR TYPESCRIPT VERSION: ${ACTIVE_TYPESCRIPT_VERSION}`, - 'Please only submit bug reports when using the officially supported version.', - border, - ]; - extra.log(versionWarning.join('\n\n')); - } - warnedAboutTSVersion = true; - } -} - -/** - * ESLint (and therefore typescript-eslint) is used in both "single run"/one-time contexts, - * such as an ESLint CLI invocation, and long-running sessions (such as continuous feedback - * on a file in an IDE). - * - * When typescript-eslint handles TypeScript Program management behind the scenes, this distinction - * is important because there is significant overhead to managing the so called Watch Programs - * needed for the long-running use-case. We therefore use the following logic to figure out which - * of these contexts applies to the current execution. - */ -function inferSingleRun(options: TSESTreeOptions | undefined): void { - // Allow users to explicitly inform us of their intent to perform a single run (or not) with TSESTREE_SINGLE_RUN - if (process.env.TSESTREE_SINGLE_RUN === 'false') { - extra.singleRun = false; - return; - } - if (process.env.TSESTREE_SINGLE_RUN === 'true') { - extra.singleRun = true; - return; - } - - // Currently behind a flag while we gather real-world feedback - if (options?.allowAutomaticSingleRunInference) { - if ( - // Default to single runs for CI processes. CI=true is set by most CI providers by default. - process.env.CI === 'true' || - // This will be true for invocations such as `npx eslint ...` and `./node_modules/.bin/eslint ...` - process.argv[1].endsWith(normalize('node_modules/.bin/eslint')) - ) { - extra.singleRun = true; - return; - } - } - - /** - * We default to assuming that this run could be part of a long-running session (e.g. in an IDE) - * and watch programs will therefore be required - */ - extra.singleRun = false; } // eslint-disable-next-line @typescript-eslint/no-empty-interface @@ -433,7 +82,7 @@ function parseWithNodeMapsInternal( /** * Reset the parse configuration */ - resetExtra(); + const parseSettings = createParseSettings(code, options); /** * Ensure users do not attempt to use parse() when they need parseAndGenerateServices() @@ -445,38 +94,18 @@ function parseWithNodeMapsInternal( } /** - * Ensure the source code is a string, and store a reference to it - */ - code = enforceString(code); - extra.code = code; - - /** - * Apply the given parser options - */ - if (typeof options !== 'undefined') { - applyParserOptionsToExtra(options); - } - - /** - * Warn if the user is using an unsupported version of TypeScript - */ - warnAboutTSVersion(); - - /** - * Figure out whether this is a single run or part of a long-running process + * Create a ts.SourceFile directly, no ts.Program is needed for a simple parse */ - inferSingleRun(options); - - /** - * Create a ts.SourceFile directly, no ts.Program is needed for a simple - * parse - */ - const ast = createSourceFile(code, extra); + const ast = createSourceFile(parseSettings); /** * Convert the TypeScript AST to an ESTree-compatible one */ - const { estree, astMaps } = astConverter(ast, extra, shouldPreserveNodeMaps); + const { estree, astMaps } = astConverter( + ast, + parseSettings, + shouldPreserveNodeMaps, + ); return { ast: estree as AST, @@ -505,47 +134,31 @@ function parseAndGenerateServices( /** * Reset the parse configuration */ - resetExtra(); + const parseSettings = createParseSettings(code, options); - /** - * Ensure the source code is a string, and store a reference to it - */ - code = enforceString(code); - extra.code = code; - - /** - * Apply the given parser options - */ if (typeof options !== 'undefined') { - applyParserOptionsToExtra(options); if ( typeof options.errorOnTypeScriptSyntacticAndSemanticIssues === 'boolean' && options.errorOnTypeScriptSyntacticAndSemanticIssues ) { - extra.errorOnTypeScriptSyntacticAndSemanticIssues = true; + parseSettings.errorOnTypeScriptSyntacticAndSemanticIssues = true; } } - /** - * Warn if the user is using an unsupported version of TypeScript - */ - warnAboutTSVersion(); - - /** - * Figure out whether this is a single run or part of a long-running process - */ - inferSingleRun(options); - /** * If this is a single run in which the user has not provided any existing programs but there * are programs which need to be created from the provided "project" option, * create an Iterable which will lazily create the programs as needed by the iteration logic */ - if (extra.singleRun && !extra.programs && extra.projects?.length > 0) { - extra.programs = { + if ( + parseSettings.singleRun && + !parseSettings.programs && + parseSettings.projects?.length > 0 + ) { + parseSettings.programs = { *[Symbol.iterator](): Iterator { - for (const configFile of extra.projects) { + for (const configFile of parseSettings.projects) { const existingProgram = existingPrograms.get(configFile); if (existingProgram) { yield existingProgram; @@ -567,7 +180,7 @@ function parseAndGenerateServices( * Generate a full ts.Program or offer provided instances in order to be able to provide parser services, such as type-checking */ const shouldProvideParserServices = - extra.programs != null || (extra.projects && extra.projects.length > 0); + parseSettings.programs != null || parseSettings.projects?.length > 0; /** * If we are in singleRun mode but the parseAndGenerateServices() function has been called more than once for the current file, @@ -577,46 +190,38 @@ function parseAndGenerateServices( * In this scenario we cannot rely upon the singleRun AOT compiled programs because the SourceFiles will not contain the source * with the latest fixes applied. Therefore we fallback to creating the quickest possible isolated program from the updated source. */ - let ast: ts.SourceFile; - let program: ts.Program; - - if (extra.singleRun && options.filePath) { + if (parseSettings.singleRun && options.filePath) { parseAndGenerateServicesCalls[options.filePath] = (parseAndGenerateServicesCalls[options.filePath] || 0) + 1; } - if ( - extra.singleRun && + const { ast, program } = + parseSettings.singleRun && options.filePath && parseAndGenerateServicesCalls[options.filePath] > 1 - ) { - const isolatedAstAndProgram = createIsolatedProgram(code, extra); - ast = isolatedAstAndProgram.ast; - program = isolatedAstAndProgram.program; - } else { - const astAndProgram = getProgramAndAST( - code, - extra.programs, - shouldProvideParserServices, - extra.createDefaultProgram, - )!; - ast = astAndProgram.ast; - program = astAndProgram.program; - } + ? createIsolatedProgram(parseSettings) + : getProgramAndAST(parseSettings, shouldProvideParserServices)!; /** * Convert the TypeScript AST to an ESTree-compatible one, and optionally preserve * mappings between converted and original AST nodes */ - const preserveNodeMaps = - typeof extra.preserveNodeMaps === 'boolean' ? extra.preserveNodeMaps : true; - const { estree, astMaps } = astConverter(ast, extra, preserveNodeMaps); + const shouldPreserveNodeMaps = + typeof parseSettings.preserveNodeMaps === 'boolean' + ? parseSettings.preserveNodeMaps + : true; + + const { estree, astMaps } = astConverter( + ast, + parseSettings, + shouldPreserveNodeMaps, + ); /** * Even if TypeScript parsed the source code ok, and we had no problems converting the AST, * there may be other syntactic or semantic issues in the code that we can optionally report on. */ - if (program && extra.errorOnTypeScriptSyntacticAndSemanticIssues) { + if (program && parseSettings.errorOnTypeScriptSyntacticAndSemanticIssues) { const error = getFirstSemanticOrSyntacticError(program, ast); if (error) { throw convertError(error); diff --git a/packages/website/src/components/linter/WebLinter.ts b/packages/website/src/components/linter/WebLinter.ts index e3fa23cf0e0d..378e34ed1361 100644 --- a/packages/website/src/components/linter/WebLinter.ts +++ b/packages/website/src/components/linter/WebLinter.ts @@ -1,5 +1,5 @@ import { createVirtualCompilerHost } from '@site/src/components/linter/CompilerHost'; -import { extra } from '@site/src/components/linter/config'; +import { parseSettings } from '@site/src/components/linter/config'; import type { ParserOptions } from '@typescript-eslint/types'; import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; import type { LintUtils } from '@typescript-eslint/website-eslint'; @@ -106,7 +106,7 @@ export class WebLinter { const { estree: ast, astMaps } = this.lintUtils.astConverter( tsAst, - { ...extra, code, jsx: isJsx }, + { ...parseSettings, code, jsx: isJsx }, true, ); diff --git a/packages/website/src/components/linter/config.ts b/packages/website/src/components/linter/config.ts index cad38a3e28fa..f077f3786ee1 100644 --- a/packages/website/src/components/linter/config.ts +++ b/packages/website/src/components/linter/config.ts @@ -1,12 +1,11 @@ -import type { Extra } from '@typescript-eslint/typescript-estree/dist/parser-options'; +import type { ParseSettings } from '@typescript-eslint/typescript-estree/dist/parseSettings'; -export const extra: Extra = { +export const parseSettings: ParseSettings = { code: '', comment: true, comments: [], createDefaultProgram: false, debugLevel: new Set(), - errorOnTypeScriptSyntacticAndSemanticIssues: false, errorOnUnknownASTType: false, extraFileExtensions: [], filePath: '', @@ -17,9 +16,9 @@ export const extra: Extra = { preserveNodeMaps: true, projects: [], range: true, - strict: false, tokens: [], tsconfigRootDir: '/', + errorOnTypeScriptSyntacticAndSemanticIssues: false, EXPERIMENTAL_useSourceOfProjectReferenceRedirect: false, singleRun: false, programs: null, From 8794fd3773af8d6a65c1c33db80a28be0a4401be Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Tue, 25 Oct 2022 18:22:57 -0400 Subject: [PATCH 06/20] chore(website): fix renamed Sponsorship docs link (#5882) --- packages/website/src/components/FinancialContributors/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/website/src/components/FinancialContributors/index.tsx b/packages/website/src/components/FinancialContributors/index.tsx index 3d5889ecedef..609de140c9a1 100644 --- a/packages/website/src/components/FinancialContributors/index.tsx +++ b/packages/website/src/components/FinancialContributors/index.tsx @@ -50,7 +50,7 @@ export function FinancialContributors(): JSX.Element { Docs From fcf3f9da6595f8cda843e8adbfdf466c52dd7d08 Mon Sep 17 00:00:00 2001 From: Adnan Hashmi <56730784+adnanhashmi09@users.noreply.github.com> Date: Wed, 26 Oct 2022 05:05:35 +0530 Subject: [PATCH 07/20] docs: Mention wide globs performance implications in monorepos docs and parser README (#5864) * docs: Mention wide globs performance implications in monorepos docs and parser readme * Update docs/linting/typed-linting/MONOREPOS.md Co-authored-by: Josh Goldberg --- docs/linting/typed-linting/MONOREPOS.md | 27 +++++++++++++++++++++++++ packages/parser/README.md | 2 ++ 2 files changed, 29 insertions(+) diff --git a/docs/linting/typed-linting/MONOREPOS.md b/docs/linting/typed-linting/MONOREPOS.md index 1bb057718c68..5163e997ec84 100644 --- a/docs/linting/typed-linting/MONOREPOS.md +++ b/docs/linting/typed-linting/MONOREPOS.md @@ -62,6 +62,33 @@ module.exports = { }; ``` +### Wide globs in `parserOptions.project` + +Using wide globs `**` in your `parserOptions.project` may degrade linting performance. +Instead of globs that use `**` to recursively check all folders, prefer paths that use a single `*` at a time. + +```js title=".eslintrc.js" +module.exports = { + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:@typescript-eslint/recommended-requiring-type-checking', + ], + parser: '@typescript-eslint/parser', + parserOptions: { + tsconfigRootDir: __dirname, + // Remove this line + project: ['./tsconfig.eslint.json', './**/tsconfig.json'], + // Add this line + project: ['./tsconfig.eslint.json', './packages/*/tsconfig.json'], + }, + plugins: ['@typescript-eslint'], + root: true, +}; +``` + +See [Glob pattern in parser's option "project" slows down linting](https://github.com/typescript-eslint/typescript-eslint/issues/2611) for more details. + ### Important note regarding large (> 10) multi-package monorepos We've had reports that for sufficiently large and/or interdependent projects, you may run into OOMs using this approach. diff --git a/packages/parser/README.md b/packages/parser/README.md index efba4854413f..b32ed30beab4 100644 --- a/packages/parser/README.md +++ b/packages/parser/README.md @@ -164,6 +164,8 @@ This option allows you to provide a path to your project's `tsconfig.json`. **Th - If you use project references, TypeScript will not automatically use project references to resolve files. This means that you will have to add each referenced tsconfig to the `project` field either separately, or via a glob. +- Note that using wide globs `**` in your `parserOptions.project` may cause performance implications. Instead of globs that use `**` to recursively check all folders, prefer paths that use a single `*` at a time. For more info see [#2611](https://github.com/typescript-eslint/typescript-eslint/issues/2611). + - TypeScript will ignore files with duplicate filenames in the same folder (for example, `src/file.ts` and `src/file.js`). TypeScript purposely ignore all but one of the files, only keeping the one file with the highest priority extension (the extension priority order (from highest to lowest) is `.ts`, `.tsx`, `.d.ts`, `.js`, `.jsx`). For more info see #955. - 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: From 2019c2f64e984e7726ea15da9b027040aaed576a Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Tue, 25 Oct 2022 22:02:37 -0400 Subject: [PATCH 08/20] chore: add auto-canary release for v6 (#5883) --- .github/workflows/ci.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ff735911dfa8..1a1fd0e460b2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,6 +4,7 @@ on: push: branches: - main + - v6 pull_request: branches: - '**' @@ -249,3 +250,28 @@ jobs: run: npx lerna publish --loglevel=verbose --canary --exact --force-publish --yes env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + publish_canary_version_v6: + name: Publish the next major version code as a canary version + runs-on: ubuntu-latest + needs: [integration_tests, lint_with_build, lint_without_build, unit_tests] + if: github.ref == 'refs/heads/v${major}' + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Install + uses: ./.github/actions/prepare-install + with: + node-version: ${{ env.PRIMARY_NODE_VERSION }} + registry-url: 'https://registry.npmjs.org' + - name: Build + uses: ./.github/actions/prepare-build + + # Fetch all history for all tags and branches in this job because lerna needs it + - run: | + git fetch --prune --unshallow + + - name: Publish all packages to npm + run: npx lerna publish premajor --loglevel=verbose --canary --exact --force-publish --yes --dist-tag rc-v${major} + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} From 3b8d449696c319690536a18a48ef32749dc2f559 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Tue, 25 Oct 2022 22:52:59 -0400 Subject: [PATCH 09/20] feat(scope-manager): ignore ECMA version (#5881) * feat(scope-manager): ignore ECMA version * Remove much more * Fix WebLinter lint --- packages/parser/src/parser.ts | 1 - packages/parser/tests/lib/parser.ts | 8 --- packages/scope-manager/README.md | 12 +---- packages/scope-manager/src/ScopeManager.ts | 9 ++-- packages/scope-manager/src/analyze.ts | 35 ++----------- .../src/referencer/Referencer.ts | 17 ++----- .../get-declared-variables.test.ts | 1 - .../tests/eslint-scope/implied-strict.test.ts | 30 +---------- .../eslint-scope/map-ecma-version.test.ts | 51 ------------------- packages/scope-manager/tests/fixtures.test.ts | 1 - .../src/components/linter/WebLinter.ts | 4 -- 11 files changed, 18 insertions(+), 151 deletions(-) delete mode 100644 packages/scope-manager/tests/eslint-scope/map-ecma-version.test.ts diff --git a/packages/parser/src/parser.ts b/packages/parser/src/parser.ts index 0e4b7780c170..e7223e933329 100644 --- a/packages/parser/src/parser.ts +++ b/packages/parser/src/parser.ts @@ -105,7 +105,6 @@ function parseForESLint( jsx: validateBoolean(options.ecmaFeatures.jsx), }); const analyzeOptions: AnalyzeOptions = { - ecmaVersion: options.ecmaVersion === 'latest' ? 1e8 : options.ecmaVersion, globalReturn: options.ecmaFeatures.globalReturn, jsxPragma: options.jsxPragma, jsxFragmentName: options.jsxFragmentName, diff --git a/packages/parser/tests/lib/parser.ts b/packages/parser/tests/lib/parser.ts index 7f2f193e11da..e554c4bfde7c 100644 --- a/packages/parser/tests/lib/parser.ts +++ b/packages/parser/tests/lib/parser.ts @@ -19,11 +19,6 @@ describe('parser', () => { expect(() => parseForESLint(code, null)).not.toThrow(); }); - it("parseForESLint() should work if options.ecmaVersion is `'latest'`", () => { - const code = 'const valid = true;'; - expect(() => parseForESLint(code, { ecmaVersion: 'latest' })).not.toThrow(); - }); - it('parseAndGenerateServices() should be called with options', () => { const code = 'const valid = true;'; const spy = jest.spyOn(typescriptESTree, 'parseAndGenerateServices'); @@ -33,7 +28,6 @@ describe('parser', () => { range: false, tokens: false, sourceType: 'module' as const, - ecmaVersion: 2018, ecmaFeatures: { globalReturn: false, jsx: false, @@ -84,7 +78,6 @@ describe('parser', () => { range: false, tokens: false, sourceType: 'module' as const, - ecmaVersion: 2018, ecmaFeatures: { globalReturn: false, jsx: false, @@ -104,7 +97,6 @@ describe('parser', () => { parseForESLint(code, config); expect(spy).toHaveBeenCalledTimes(1); expect(spy).toHaveBeenLastCalledWith(expect.anything(), { - ecmaVersion: 2018, globalReturn: false, lib: ['dom.iterable'], jsxPragma: 'Foo', diff --git a/packages/scope-manager/README.md b/packages/scope-manager/README.md index b0a745f3fa53..3d9ef751032f 100644 --- a/packages/scope-manager/README.md +++ b/packages/scope-manager/README.md @@ -36,13 +36,6 @@ interface AnalyzeOptions { */ childVisitorKeys?: Record | null; - /** - * Which ECMAScript version is considered. - * Defaults to `2018`. - * `'latest'` is converted to 1e8 at parser. - */ - ecmaVersion?: EcmaVersion | 1e8; - /** * Whether the whole script is executed under node.js environment. * When enabled, the scope manager adds a function scope immediately following the global scope. @@ -51,7 +44,7 @@ interface AnalyzeOptions { globalReturn?: boolean; /** - * Implied strict mode (if ecmaVersion >= 5). + * Implied strict mode. * Defaults to `false`. */ impliedStrict?: boolean; @@ -76,7 +69,7 @@ interface AnalyzeOptions { * This automatically defines a type variable for any types provided by the configured TS libs. * For more information, see https://www.typescriptlang.org/tsconfig#lib * - * Defaults to the lib for the provided `ecmaVersion`. + * Defaults to ['esnext']. */ lib?: Lib[]; @@ -105,7 +98,6 @@ const ast = parse(code, { range: true, }); const scope = analyze(ast, { - ecmaVersion: 2020, sourceType: 'module', }); ``` diff --git a/packages/scope-manager/src/ScopeManager.ts b/packages/scope-manager/src/ScopeManager.ts index 5368cca1dc3b..d8beafba4aad 100644 --- a/packages/scope-manager/src/ScopeManager.ts +++ b/packages/scope-manager/src/ScopeManager.ts @@ -28,9 +28,11 @@ interface ScopeManagerOptions { globalReturn?: boolean; sourceType?: 'module' | 'script'; impliedStrict?: boolean; - ecmaVersion?: number; } +/** + * @see https://eslint.org/docs/latest/developer-guide/scope-manager-interface#scopemanager-interface + */ class ScopeManager { public currentScope: Scope | null; public readonly declaredVariables: WeakMap; @@ -77,12 +79,13 @@ class ScopeManager { public isImpliedStrict(): boolean { return this.#options.impliedStrict === true; } + public isStrictModeSupported(): boolean { - return this.#options.ecmaVersion != null && this.#options.ecmaVersion >= 5; + return true; } public isES6(): boolean { - return this.#options.ecmaVersion != null && this.#options.ecmaVersion >= 6; + return true; } /** diff --git a/packages/scope-manager/src/analyze.ts b/packages/scope-manager/src/analyze.ts index e227d1e45ad3..1cb1d989209f 100644 --- a/packages/scope-manager/src/analyze.ts +++ b/packages/scope-manager/src/analyze.ts @@ -1,7 +1,6 @@ -import type { EcmaVersion, Lib, TSESTree } from '@typescript-eslint/types'; +import type { Lib, TSESTree } from '@typescript-eslint/types'; import { visitorKeys } from '@typescript-eslint/visitor-keys'; -import { lib as TSLibraries } from './lib'; import type { ReferencerOptions } from './referencer'; import { Referencer } from './referencer'; import { ScopeManager } from './ScopeManager'; @@ -16,13 +15,6 @@ interface AnalyzeOptions { */ childVisitorKeys?: ReferencerOptions['childVisitorKeys']; - /** - * Which ECMAScript version is considered. - * Defaults to `2018`. - * `'latest'` is converted to 1e8 at parser. - */ - ecmaVersion?: EcmaVersion | 1e8; - /** * Whether the whole script is executed under node.js environment. * When enabled, the scope manager adds a function scope immediately following the global scope. @@ -31,7 +23,7 @@ interface AnalyzeOptions { globalReturn?: boolean; /** - * Implied strict mode (if ecmaVersion >= 5). + * Implied strict mode. * Defaults to `false`. */ impliedStrict?: boolean; @@ -54,7 +46,7 @@ interface AnalyzeOptions { /** * The lib used by the project. * This automatically defines a type variable for any types provided by the configured TS libs. - * Defaults to the lib for the provided `ecmaVersion`. + * Defaults to ['esnext']. * * https://www.typescriptlang.org/tsconfig#lib */ @@ -74,7 +66,6 @@ interface AnalyzeOptions { const DEFAULT_OPTIONS: Required = { childVisitorKeys: visitorKeys, - ecmaVersion: 2018, globalReturn: false, impliedStrict: false, jsxPragma: 'React', @@ -84,21 +75,6 @@ const DEFAULT_OPTIONS: Required = { emitDecoratorMetadata: false, }; -/** - * Convert ecmaVersion to lib. - * `'latest'` is converted to 1e8 at parser. - */ -function mapEcmaVersion(version: EcmaVersion | 1e8 | undefined): Lib { - if (version == null || version === 3 || version === 5) { - return 'es5'; - } - - const year = version > 2000 ? version : 2015 + (version - 6); - const lib = `es${year}`; - - return lib in TSLibraries ? (lib as Lib) : year > 2020 ? 'esnext' : 'es5'; -} - /** * Takes an AST and returns the analyzed scopes. */ @@ -106,12 +82,9 @@ function analyze( tree: TSESTree.Node, providedOptions?: AnalyzeOptions, ): ScopeManager { - const ecmaVersion = - providedOptions?.ecmaVersion ?? DEFAULT_OPTIONS.ecmaVersion; const options: Required = { childVisitorKeys: providedOptions?.childVisitorKeys ?? DEFAULT_OPTIONS.childVisitorKeys, - ecmaVersion, globalReturn: providedOptions?.globalReturn ?? DEFAULT_OPTIONS.globalReturn, impliedStrict: providedOptions?.impliedStrict ?? DEFAULT_OPTIONS.impliedStrict, @@ -122,7 +95,7 @@ function analyze( jsxFragmentName: providedOptions?.jsxFragmentName ?? DEFAULT_OPTIONS.jsxFragmentName, sourceType: providedOptions?.sourceType ?? DEFAULT_OPTIONS.sourceType, - lib: providedOptions?.lib ?? [mapEcmaVersion(ecmaVersion)], + lib: providedOptions?.lib ?? ['esnext'], emitDecoratorMetadata: providedOptions?.emitDecoratorMetadata ?? DEFAULT_OPTIONS.emitDecoratorMetadata, diff --git a/packages/scope-manager/src/referencer/Referencer.ts b/packages/scope-manager/src/referencer/Referencer.ts index a69209e86c6f..ecadb7e9fd64 100644 --- a/packages/scope-manager/src/referencer/Referencer.ts +++ b/packages/scope-manager/src/referencer/Referencer.ts @@ -371,9 +371,7 @@ class Referencer extends Visitor { } protected BlockStatement(node: TSESTree.BlockStatement): void { - if (this.scopeManager.isES6()) { - this.scopeManager.nestBlockScope(node); - } + this.scopeManager.nestBlockScope(node); this.visitChildren(node); @@ -487,7 +485,7 @@ class Referencer extends Visitor { protected ImportDeclaration(node: TSESTree.ImportDeclaration): void { assert( - this.scopeManager.isES6() && this.scopeManager.isModule(), + this.scopeManager.isModule(), 'ImportDeclaration should appear when the mode is ES6 and in the module context.', ); @@ -579,14 +577,11 @@ class Referencer extends Visitor { this.scopeManager.nestFunctionScope(node, false); } - if (this.scopeManager.isES6() && this.scopeManager.isModule()) { + if (this.scopeManager.isModule()) { this.scopeManager.nestModuleScope(node); } - if ( - this.scopeManager.isStrictModeSupported() && - this.scopeManager.isImpliedStrict() - ) { + if (this.scopeManager.isImpliedStrict()) { this.currentScope().isStrict = true; } @@ -601,9 +596,7 @@ class Referencer extends Visitor { protected SwitchStatement(node: TSESTree.SwitchStatement): void { this.visit(node.discriminant); - if (this.scopeManager.isES6()) { - this.scopeManager.nestSwitchScope(node); - } + this.scopeManager.nestSwitchScope(node); for (const switchCase of node.cases) { this.visit(switchCase); diff --git a/packages/scope-manager/tests/eslint-scope/get-declared-variables.test.ts b/packages/scope-manager/tests/eslint-scope/get-declared-variables.test.ts index 23e02b9af0cd..82611eb5147e 100644 --- a/packages/scope-manager/tests/eslint-scope/get-declared-variables.test.ts +++ b/packages/scope-manager/tests/eslint-scope/get-declared-variables.test.ts @@ -12,7 +12,6 @@ describe('ScopeManager.prototype.getDeclaredVariables', () => { expectedNamesList: string[][], ): void { const scopeManager = analyze(ast, { - ecmaVersion: 6, sourceType: 'module', }); diff --git a/packages/scope-manager/tests/eslint-scope/implied-strict.test.ts b/packages/scope-manager/tests/eslint-scope/implied-strict.test.ts index 34151be8c3da..893da6048c28 100644 --- a/packages/scope-manager/tests/eslint-scope/implied-strict.test.ts +++ b/packages/scope-manager/tests/eslint-scope/implied-strict.test.ts @@ -8,7 +8,7 @@ import { } from '../util'; describe('impliedStrict option', () => { - it('ensures all user scopes are strict if ecmaVersion >= 5', () => { + it('ensures all user scopes are strict', () => { const { scopeManager } = parseAndAnalyze( ` function foo() { @@ -18,7 +18,6 @@ describe('impliedStrict option', () => { } `, { - ecmaVersion: 5, impliedStrict: true, }, ); @@ -42,38 +41,12 @@ describe('impliedStrict option', () => { expect(scope.isStrict).toBeTruthy(); }); - it('ensures impliedStrict option is only effective when ecmaVersion option >= 5', () => { - const { scopeManager } = parseAndAnalyze( - ` - function foo() {} - `, - { - ecmaVersion: 3, - impliedStrict: true, - }, - ); - - expect(scopeManager.scopes).toHaveLength(2); - - let scope = scopeManager.scopes[0]; - - expectToBeGlobalScope(scope); - expect(scope.block.type).toBe(AST_NODE_TYPES.Program); - expect(scope.isStrict).toBeFalsy(); - - scope = scopeManager.scopes[1]; - expectToBeFunctionScope(scope); - expect(scope.block.type).toBe(AST_NODE_TYPES.FunctionDeclaration); - expect(scope.isStrict).toBeFalsy(); - }); - it('omits a nodejs global scope when ensuring all user scopes are strict', () => { const { scopeManager } = parseAndAnalyze( ` function foo() {} `, { - ecmaVersion: 5, globalReturn: true, impliedStrict: true, }, @@ -100,7 +73,6 @@ describe('impliedStrict option', () => { it('omits a module global scope when ensuring all user scopes are strict', () => { const { scopeManager } = parseAndAnalyze('function foo() {}', { - ecmaVersion: 6, impliedStrict: true, sourceType: 'module', }); diff --git a/packages/scope-manager/tests/eslint-scope/map-ecma-version.test.ts b/packages/scope-manager/tests/eslint-scope/map-ecma-version.test.ts deleted file mode 100644 index becca474ff3a..000000000000 --- a/packages/scope-manager/tests/eslint-scope/map-ecma-version.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import type { EcmaVersion, Lib, TSESTree } from '@typescript-eslint/types'; - -import { analyze } from '../../src/analyze'; -import { Referencer } from '../../src/referencer'; - -jest.mock('../../src/referencer'); -jest.mock('../../src/ScopeManager'); - -describe('ecma version mapping', () => { - it("should map to 'esnext' when unsuported and new", () => { - expectMapping(2042, 'esnext'); - expectMapping(42, 'esnext'); - }); - - it("should map to 'es5' when unsuported and old", () => { - expectMapping(2002, 'es5'); - expectMapping(2, 'es5'); - }); - - it("should map to 'es{year}' when supported and >= 6", () => { - expectMapping(2015, 'es2015'); - expectMapping(6, 'es2015'); - expectMapping(2020, 'es2020'); - expectMapping(11, 'es2020'); - }); - - it("should map to 'es5' when 5 or 3", () => { - expectMapping(5, 'es5'); - expectMapping(3, 'es5'); - }); - - it("should map to 'es2018' when undefined", () => { - expectMapping(undefined, 'es2018'); - }); - - it("should map to 'esnext' when 'latest'", () => { - // `'latest'` is converted to 1e8 at parser. - expectMapping(1e8, 'esnext'); - }); -}); - -const fakeNode = {} as unknown as TSESTree.Node; - -function expectMapping(ecmaVersion: number | undefined, lib: Lib): void { - (Referencer as jest.Mock).mockClear(); - analyze(fakeNode, { ecmaVersion: ecmaVersion as EcmaVersion }); - expect(Referencer).toHaveBeenCalledWith( - expect.objectContaining({ lib: [lib] }), - expect.any(Object), - ); -} diff --git a/packages/scope-manager/tests/fixtures.test.ts b/packages/scope-manager/tests/fixtures.test.ts index fa86c1544c7b..ac6b39c860ef 100644 --- a/packages/scope-manager/tests/fixtures.test.ts +++ b/packages/scope-manager/tests/fixtures.test.ts @@ -42,7 +42,6 @@ const ALLOWED_OPTIONS: Map = new Map< keyof AnalyzeOptions, ALLOWED_VALUE >([ - ['ecmaVersion', ['number']], ['globalReturn', ['boolean']], ['impliedStrict', ['boolean']], ['jsxPragma', ['string']], diff --git a/packages/website/src/components/linter/WebLinter.ts b/packages/website/src/components/linter/WebLinter.ts index 378e34ed1361..acd3c569ff3f 100644 --- a/packages/website/src/components/linter/WebLinter.ts +++ b/packages/website/src/components/linter/WebLinter.ts @@ -111,10 +111,6 @@ export class WebLinter { ); const scopeManager = this.lintUtils.analyze(ast, { - ecmaVersion: - eslintOptions.ecmaVersion === 'latest' - ? 1e8 - : eslintOptions.ecmaVersion, globalReturn: eslintOptions.ecmaFeatures?.globalReturn ?? false, sourceType: eslintOptions.sourceType ?? 'script', }); From 2ee81df5a365d82ef4b3dfc124d4ec39c7bcb725 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Tue, 25 Oct 2022 23:08:14 -0400 Subject: [PATCH 10/20] Revert "feat(scope-manager): ignore ECMA version" (#5888) Revert "feat(scope-manager): ignore ECMA version (#5881)" This reverts commit 3b8d449696c319690536a18a48ef32749dc2f559. --- packages/parser/src/parser.ts | 1 + packages/parser/tests/lib/parser.ts | 8 +++ packages/scope-manager/README.md | 12 ++++- packages/scope-manager/src/ScopeManager.ts | 9 ++-- packages/scope-manager/src/analyze.ts | 35 +++++++++++-- .../src/referencer/Referencer.ts | 17 +++++-- .../get-declared-variables.test.ts | 1 + .../tests/eslint-scope/implied-strict.test.ts | 30 ++++++++++- .../eslint-scope/map-ecma-version.test.ts | 51 +++++++++++++++++++ packages/scope-manager/tests/fixtures.test.ts | 1 + .../src/components/linter/WebLinter.ts | 4 ++ 11 files changed, 151 insertions(+), 18 deletions(-) create mode 100644 packages/scope-manager/tests/eslint-scope/map-ecma-version.test.ts diff --git a/packages/parser/src/parser.ts b/packages/parser/src/parser.ts index e7223e933329..0e4b7780c170 100644 --- a/packages/parser/src/parser.ts +++ b/packages/parser/src/parser.ts @@ -105,6 +105,7 @@ function parseForESLint( jsx: validateBoolean(options.ecmaFeatures.jsx), }); const analyzeOptions: AnalyzeOptions = { + ecmaVersion: options.ecmaVersion === 'latest' ? 1e8 : options.ecmaVersion, globalReturn: options.ecmaFeatures.globalReturn, jsxPragma: options.jsxPragma, jsxFragmentName: options.jsxFragmentName, diff --git a/packages/parser/tests/lib/parser.ts b/packages/parser/tests/lib/parser.ts index e554c4bfde7c..7f2f193e11da 100644 --- a/packages/parser/tests/lib/parser.ts +++ b/packages/parser/tests/lib/parser.ts @@ -19,6 +19,11 @@ describe('parser', () => { expect(() => parseForESLint(code, null)).not.toThrow(); }); + it("parseForESLint() should work if options.ecmaVersion is `'latest'`", () => { + const code = 'const valid = true;'; + expect(() => parseForESLint(code, { ecmaVersion: 'latest' })).not.toThrow(); + }); + it('parseAndGenerateServices() should be called with options', () => { const code = 'const valid = true;'; const spy = jest.spyOn(typescriptESTree, 'parseAndGenerateServices'); @@ -28,6 +33,7 @@ describe('parser', () => { range: false, tokens: false, sourceType: 'module' as const, + ecmaVersion: 2018, ecmaFeatures: { globalReturn: false, jsx: false, @@ -78,6 +84,7 @@ describe('parser', () => { range: false, tokens: false, sourceType: 'module' as const, + ecmaVersion: 2018, ecmaFeatures: { globalReturn: false, jsx: false, @@ -97,6 +104,7 @@ describe('parser', () => { parseForESLint(code, config); expect(spy).toHaveBeenCalledTimes(1); expect(spy).toHaveBeenLastCalledWith(expect.anything(), { + ecmaVersion: 2018, globalReturn: false, lib: ['dom.iterable'], jsxPragma: 'Foo', diff --git a/packages/scope-manager/README.md b/packages/scope-manager/README.md index 3d9ef751032f..b0a745f3fa53 100644 --- a/packages/scope-manager/README.md +++ b/packages/scope-manager/README.md @@ -36,6 +36,13 @@ interface AnalyzeOptions { */ childVisitorKeys?: Record | null; + /** + * Which ECMAScript version is considered. + * Defaults to `2018`. + * `'latest'` is converted to 1e8 at parser. + */ + ecmaVersion?: EcmaVersion | 1e8; + /** * Whether the whole script is executed under node.js environment. * When enabled, the scope manager adds a function scope immediately following the global scope. @@ -44,7 +51,7 @@ interface AnalyzeOptions { globalReturn?: boolean; /** - * Implied strict mode. + * Implied strict mode (if ecmaVersion >= 5). * Defaults to `false`. */ impliedStrict?: boolean; @@ -69,7 +76,7 @@ interface AnalyzeOptions { * This automatically defines a type variable for any types provided by the configured TS libs. * For more information, see https://www.typescriptlang.org/tsconfig#lib * - * Defaults to ['esnext']. + * Defaults to the lib for the provided `ecmaVersion`. */ lib?: Lib[]; @@ -98,6 +105,7 @@ const ast = parse(code, { range: true, }); const scope = analyze(ast, { + ecmaVersion: 2020, sourceType: 'module', }); ``` diff --git a/packages/scope-manager/src/ScopeManager.ts b/packages/scope-manager/src/ScopeManager.ts index d8beafba4aad..5368cca1dc3b 100644 --- a/packages/scope-manager/src/ScopeManager.ts +++ b/packages/scope-manager/src/ScopeManager.ts @@ -28,11 +28,9 @@ interface ScopeManagerOptions { globalReturn?: boolean; sourceType?: 'module' | 'script'; impliedStrict?: boolean; + ecmaVersion?: number; } -/** - * @see https://eslint.org/docs/latest/developer-guide/scope-manager-interface#scopemanager-interface - */ class ScopeManager { public currentScope: Scope | null; public readonly declaredVariables: WeakMap; @@ -79,13 +77,12 @@ class ScopeManager { public isImpliedStrict(): boolean { return this.#options.impliedStrict === true; } - public isStrictModeSupported(): boolean { - return true; + return this.#options.ecmaVersion != null && this.#options.ecmaVersion >= 5; } public isES6(): boolean { - return true; + return this.#options.ecmaVersion != null && this.#options.ecmaVersion >= 6; } /** diff --git a/packages/scope-manager/src/analyze.ts b/packages/scope-manager/src/analyze.ts index 1cb1d989209f..e227d1e45ad3 100644 --- a/packages/scope-manager/src/analyze.ts +++ b/packages/scope-manager/src/analyze.ts @@ -1,6 +1,7 @@ -import type { Lib, TSESTree } from '@typescript-eslint/types'; +import type { EcmaVersion, Lib, TSESTree } from '@typescript-eslint/types'; import { visitorKeys } from '@typescript-eslint/visitor-keys'; +import { lib as TSLibraries } from './lib'; import type { ReferencerOptions } from './referencer'; import { Referencer } from './referencer'; import { ScopeManager } from './ScopeManager'; @@ -15,6 +16,13 @@ interface AnalyzeOptions { */ childVisitorKeys?: ReferencerOptions['childVisitorKeys']; + /** + * Which ECMAScript version is considered. + * Defaults to `2018`. + * `'latest'` is converted to 1e8 at parser. + */ + ecmaVersion?: EcmaVersion | 1e8; + /** * Whether the whole script is executed under node.js environment. * When enabled, the scope manager adds a function scope immediately following the global scope. @@ -23,7 +31,7 @@ interface AnalyzeOptions { globalReturn?: boolean; /** - * Implied strict mode. + * Implied strict mode (if ecmaVersion >= 5). * Defaults to `false`. */ impliedStrict?: boolean; @@ -46,7 +54,7 @@ interface AnalyzeOptions { /** * The lib used by the project. * This automatically defines a type variable for any types provided by the configured TS libs. - * Defaults to ['esnext']. + * Defaults to the lib for the provided `ecmaVersion`. * * https://www.typescriptlang.org/tsconfig#lib */ @@ -66,6 +74,7 @@ interface AnalyzeOptions { const DEFAULT_OPTIONS: Required = { childVisitorKeys: visitorKeys, + ecmaVersion: 2018, globalReturn: false, impliedStrict: false, jsxPragma: 'React', @@ -75,6 +84,21 @@ const DEFAULT_OPTIONS: Required = { emitDecoratorMetadata: false, }; +/** + * Convert ecmaVersion to lib. + * `'latest'` is converted to 1e8 at parser. + */ +function mapEcmaVersion(version: EcmaVersion | 1e8 | undefined): Lib { + if (version == null || version === 3 || version === 5) { + return 'es5'; + } + + const year = version > 2000 ? version : 2015 + (version - 6); + const lib = `es${year}`; + + return lib in TSLibraries ? (lib as Lib) : year > 2020 ? 'esnext' : 'es5'; +} + /** * Takes an AST and returns the analyzed scopes. */ @@ -82,9 +106,12 @@ function analyze( tree: TSESTree.Node, providedOptions?: AnalyzeOptions, ): ScopeManager { + const ecmaVersion = + providedOptions?.ecmaVersion ?? DEFAULT_OPTIONS.ecmaVersion; const options: Required = { childVisitorKeys: providedOptions?.childVisitorKeys ?? DEFAULT_OPTIONS.childVisitorKeys, + ecmaVersion, globalReturn: providedOptions?.globalReturn ?? DEFAULT_OPTIONS.globalReturn, impliedStrict: providedOptions?.impliedStrict ?? DEFAULT_OPTIONS.impliedStrict, @@ -95,7 +122,7 @@ function analyze( jsxFragmentName: providedOptions?.jsxFragmentName ?? DEFAULT_OPTIONS.jsxFragmentName, sourceType: providedOptions?.sourceType ?? DEFAULT_OPTIONS.sourceType, - lib: providedOptions?.lib ?? ['esnext'], + lib: providedOptions?.lib ?? [mapEcmaVersion(ecmaVersion)], emitDecoratorMetadata: providedOptions?.emitDecoratorMetadata ?? DEFAULT_OPTIONS.emitDecoratorMetadata, diff --git a/packages/scope-manager/src/referencer/Referencer.ts b/packages/scope-manager/src/referencer/Referencer.ts index ecadb7e9fd64..a69209e86c6f 100644 --- a/packages/scope-manager/src/referencer/Referencer.ts +++ b/packages/scope-manager/src/referencer/Referencer.ts @@ -371,7 +371,9 @@ class Referencer extends Visitor { } protected BlockStatement(node: TSESTree.BlockStatement): void { - this.scopeManager.nestBlockScope(node); + if (this.scopeManager.isES6()) { + this.scopeManager.nestBlockScope(node); + } this.visitChildren(node); @@ -485,7 +487,7 @@ class Referencer extends Visitor { protected ImportDeclaration(node: TSESTree.ImportDeclaration): void { assert( - this.scopeManager.isModule(), + this.scopeManager.isES6() && this.scopeManager.isModule(), 'ImportDeclaration should appear when the mode is ES6 and in the module context.', ); @@ -577,11 +579,14 @@ class Referencer extends Visitor { this.scopeManager.nestFunctionScope(node, false); } - if (this.scopeManager.isModule()) { + if (this.scopeManager.isES6() && this.scopeManager.isModule()) { this.scopeManager.nestModuleScope(node); } - if (this.scopeManager.isImpliedStrict()) { + if ( + this.scopeManager.isStrictModeSupported() && + this.scopeManager.isImpliedStrict() + ) { this.currentScope().isStrict = true; } @@ -596,7 +601,9 @@ class Referencer extends Visitor { protected SwitchStatement(node: TSESTree.SwitchStatement): void { this.visit(node.discriminant); - this.scopeManager.nestSwitchScope(node); + if (this.scopeManager.isES6()) { + this.scopeManager.nestSwitchScope(node); + } for (const switchCase of node.cases) { this.visit(switchCase); diff --git a/packages/scope-manager/tests/eslint-scope/get-declared-variables.test.ts b/packages/scope-manager/tests/eslint-scope/get-declared-variables.test.ts index 82611eb5147e..23e02b9af0cd 100644 --- a/packages/scope-manager/tests/eslint-scope/get-declared-variables.test.ts +++ b/packages/scope-manager/tests/eslint-scope/get-declared-variables.test.ts @@ -12,6 +12,7 @@ describe('ScopeManager.prototype.getDeclaredVariables', () => { expectedNamesList: string[][], ): void { const scopeManager = analyze(ast, { + ecmaVersion: 6, sourceType: 'module', }); diff --git a/packages/scope-manager/tests/eslint-scope/implied-strict.test.ts b/packages/scope-manager/tests/eslint-scope/implied-strict.test.ts index 893da6048c28..34151be8c3da 100644 --- a/packages/scope-manager/tests/eslint-scope/implied-strict.test.ts +++ b/packages/scope-manager/tests/eslint-scope/implied-strict.test.ts @@ -8,7 +8,7 @@ import { } from '../util'; describe('impliedStrict option', () => { - it('ensures all user scopes are strict', () => { + it('ensures all user scopes are strict if ecmaVersion >= 5', () => { const { scopeManager } = parseAndAnalyze( ` function foo() { @@ -18,6 +18,7 @@ describe('impliedStrict option', () => { } `, { + ecmaVersion: 5, impliedStrict: true, }, ); @@ -41,12 +42,38 @@ describe('impliedStrict option', () => { expect(scope.isStrict).toBeTruthy(); }); + it('ensures impliedStrict option is only effective when ecmaVersion option >= 5', () => { + const { scopeManager } = parseAndAnalyze( + ` + function foo() {} + `, + { + ecmaVersion: 3, + impliedStrict: true, + }, + ); + + expect(scopeManager.scopes).toHaveLength(2); + + let scope = scopeManager.scopes[0]; + + expectToBeGlobalScope(scope); + expect(scope.block.type).toBe(AST_NODE_TYPES.Program); + expect(scope.isStrict).toBeFalsy(); + + scope = scopeManager.scopes[1]; + expectToBeFunctionScope(scope); + expect(scope.block.type).toBe(AST_NODE_TYPES.FunctionDeclaration); + expect(scope.isStrict).toBeFalsy(); + }); + it('omits a nodejs global scope when ensuring all user scopes are strict', () => { const { scopeManager } = parseAndAnalyze( ` function foo() {} `, { + ecmaVersion: 5, globalReturn: true, impliedStrict: true, }, @@ -73,6 +100,7 @@ describe('impliedStrict option', () => { it('omits a module global scope when ensuring all user scopes are strict', () => { const { scopeManager } = parseAndAnalyze('function foo() {}', { + ecmaVersion: 6, impliedStrict: true, sourceType: 'module', }); diff --git a/packages/scope-manager/tests/eslint-scope/map-ecma-version.test.ts b/packages/scope-manager/tests/eslint-scope/map-ecma-version.test.ts new file mode 100644 index 000000000000..becca474ff3a --- /dev/null +++ b/packages/scope-manager/tests/eslint-scope/map-ecma-version.test.ts @@ -0,0 +1,51 @@ +import type { EcmaVersion, Lib, TSESTree } from '@typescript-eslint/types'; + +import { analyze } from '../../src/analyze'; +import { Referencer } from '../../src/referencer'; + +jest.mock('../../src/referencer'); +jest.mock('../../src/ScopeManager'); + +describe('ecma version mapping', () => { + it("should map to 'esnext' when unsuported and new", () => { + expectMapping(2042, 'esnext'); + expectMapping(42, 'esnext'); + }); + + it("should map to 'es5' when unsuported and old", () => { + expectMapping(2002, 'es5'); + expectMapping(2, 'es5'); + }); + + it("should map to 'es{year}' when supported and >= 6", () => { + expectMapping(2015, 'es2015'); + expectMapping(6, 'es2015'); + expectMapping(2020, 'es2020'); + expectMapping(11, 'es2020'); + }); + + it("should map to 'es5' when 5 or 3", () => { + expectMapping(5, 'es5'); + expectMapping(3, 'es5'); + }); + + it("should map to 'es2018' when undefined", () => { + expectMapping(undefined, 'es2018'); + }); + + it("should map to 'esnext' when 'latest'", () => { + // `'latest'` is converted to 1e8 at parser. + expectMapping(1e8, 'esnext'); + }); +}); + +const fakeNode = {} as unknown as TSESTree.Node; + +function expectMapping(ecmaVersion: number | undefined, lib: Lib): void { + (Referencer as jest.Mock).mockClear(); + analyze(fakeNode, { ecmaVersion: ecmaVersion as EcmaVersion }); + expect(Referencer).toHaveBeenCalledWith( + expect.objectContaining({ lib: [lib] }), + expect.any(Object), + ); +} diff --git a/packages/scope-manager/tests/fixtures.test.ts b/packages/scope-manager/tests/fixtures.test.ts index ac6b39c860ef..fa86c1544c7b 100644 --- a/packages/scope-manager/tests/fixtures.test.ts +++ b/packages/scope-manager/tests/fixtures.test.ts @@ -42,6 +42,7 @@ const ALLOWED_OPTIONS: Map = new Map< keyof AnalyzeOptions, ALLOWED_VALUE >([ + ['ecmaVersion', ['number']], ['globalReturn', ['boolean']], ['impliedStrict', ['boolean']], ['jsxPragma', ['string']], diff --git a/packages/website/src/components/linter/WebLinter.ts b/packages/website/src/components/linter/WebLinter.ts index acd3c569ff3f..378e34ed1361 100644 --- a/packages/website/src/components/linter/WebLinter.ts +++ b/packages/website/src/components/linter/WebLinter.ts @@ -111,6 +111,10 @@ export class WebLinter { ); const scopeManager = this.lintUtils.analyze(ast, { + ecmaVersion: + eslintOptions.ecmaVersion === 'latest' + ? 1e8 + : eslintOptions.ecmaVersion, globalReturn: eslintOptions.ecmaFeatures?.globalReturn ?? false, sourceType: eslintOptions.sourceType ?? 'script', }); From a0c828559187d1e167f2e095b503c887db4d4352 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Thu, 27 Oct 2022 01:02:45 -0400 Subject: [PATCH 11/20] feat(eslint-plugin) [sort-type-union-intersection-members] rename to sort-type-constituents (#5879) * feat(eslint-plugin) [sort-type-union-intersection-members] rename to sort-type-constituents * Sigh, test rename * Added back rule, with replacedBy * Fixed up rules exports --- .eslintrc.js | 2 +- .../docs/rules/sort-type-constituents.md | 101 ++++++ .../sort-type-union-intersection-members.md | 5 + packages/eslint-plugin/src/configs/all.ts | 2 +- packages/eslint-plugin/src/rules/index.ts | 2 + .../src/rules/sort-type-constituents.ts | 269 ++++++++++++++ .../sort-type-union-intersection-members.ts | 2 + .../rules/sort-type-constituents.test.ts | 334 ++++++++++++++++++ 8 files changed, 715 insertions(+), 2 deletions(-) create mode 100644 packages/eslint-plugin/docs/rules/sort-type-constituents.md create mode 100644 packages/eslint-plugin/src/rules/sort-type-constituents.ts create mode 100644 packages/eslint-plugin/tests/rules/sort-type-constituents.test.ts diff --git a/.eslintrc.js b/.eslintrc.js index 8e62b6d06e39..da7ff36fd3b2 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -308,7 +308,7 @@ module.exports = { rules: { // disallow ALL unused vars '@typescript-eslint/no-unused-vars': 'error', - '@typescript-eslint/sort-type-union-intersection-members': 'error', + '@typescript-eslint/sort-type-constituents': 'error', }, }, { diff --git a/packages/eslint-plugin/docs/rules/sort-type-constituents.md b/packages/eslint-plugin/docs/rules/sort-type-constituents.md new file mode 100644 index 000000000000..264ef2b52df9 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/sort-type-constituents.md @@ -0,0 +1,101 @@ +--- +description: 'Enforce constituents of a type union/intersection to be sorted alphabetically.' +--- + +> 🛑 This file is source code, not the primary documentation location! 🛑 +> +> See **https://typescript-eslint.io/rules/sort-type-constituents** for documentation. + +Sorting union (`|`) and intersection (`&`) types can help: + +- keep your codebase standardized +- find repeated types +- reduce diff churn + +This rule reports on any types that aren't sorted alphabetically. + +> Types are sorted case-insensitively and treating numbers like a human would, falling back to character code sorting in case of ties. + +## Examples + + + +### ❌ Incorrect + +```ts +type T1 = B | A; + +type T2 = { b: string } & { a: string }; + +type T3 = [1, 2, 4] & [1, 2, 3]; + +type T4 = + | [1, 2, 4] + | [1, 2, 3] + | { b: string } + | { a: string } + | (() => void) + | (() => string) + | 'b' + | 'a' + | 'b' + | 'a' + | readonly string[] + | readonly number[] + | string[] + | number[] + | B + | A + | string + | any; +``` + +### ✅ Correct + +```ts +type T1 = A | B; + +type T2 = { a: string } & { b: string }; + +type T3 = [1, 2, 3] & [1, 2, 4]; + +type T4 = + | any + | string + | A + | B + | number[] + | string[] + | readonly number[] + | readonly string[] + | 'a' + | 'b' + | 'a' + | 'b' + | (() => string) + | (() => void) + | { a: string } + | { b: string } + | [1, 2, 3] + | [1, 2, 4]; +``` + +## Options + +### `groupOrder` + +Each constituent of the type is placed into a group, and then the rule sorts alphabetically within each group. +The ordering of groups is determined by this option. + +- `conditional` - Conditional types (`A extends B ? C : D`) +- `function` - Function and constructor types (`() => void`, `new () => type`) +- `import` - Import types (`import('path')`) +- `intersection` - Intersection types (`A & B`) +- `keyword` - Keyword types (`any`, `string`, etc) +- `literal` - Literal types (`1`, `'b'`, `true`, etc) +- `named` - Named types (`A`, `A['prop']`, `B[]`, `Array`) +- `object` - Object types (`{ a: string }`, `{ [key: string]: number }`) +- `operator` - Operator types (`keyof A`, `typeof B`, `readonly C[]`) +- `tuple` - Tuple types (`[A, B, C]`) +- `union` - Union types (`A | B`) +- `nullish` - `null` and `undefined` diff --git a/packages/eslint-plugin/docs/rules/sort-type-union-intersection-members.md b/packages/eslint-plugin/docs/rules/sort-type-union-intersection-members.md index 2a47547219bf..dbaf1807edff 100644 --- a/packages/eslint-plugin/docs/rules/sort-type-union-intersection-members.md +++ b/packages/eslint-plugin/docs/rules/sort-type-union-intersection-members.md @@ -6,6 +6,11 @@ description: 'Enforce members of a type union/intersection to be sorted alphabet > > See **https://typescript-eslint.io/rules/sort-type-union-intersection-members** for documentation. +:::danger Deprecated + +This rule has been renamed to [`sort-type-union-intersection-members`](./sort-type-union-intersection-members.md). +::: + Sorting union (`|`) and intersection (`&`) types can help: - keep your codebase standardized diff --git a/packages/eslint-plugin/src/configs/all.ts b/packages/eslint-plugin/src/configs/all.ts index 1f6530ead3ec..20ea892f581d 100644 --- a/packages/eslint-plugin/src/configs/all.ts +++ b/packages/eslint-plugin/src/configs/all.ts @@ -154,7 +154,7 @@ export = { '@typescript-eslint/return-await': 'error', semi: 'off', '@typescript-eslint/semi': 'error', - '@typescript-eslint/sort-type-union-intersection-members': 'error', + '@typescript-eslint/sort-type-constituents': 'error', 'space-before-blocks': 'off', '@typescript-eslint/space-before-blocks': 'error', 'space-before-function-paren': 'off', diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index 851661400fbb..8a3c2bbf4371 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -115,6 +115,7 @@ import restrictPlusOperands from './restrict-plus-operands'; import restrictTemplateExpressions from './restrict-template-expressions'; import returnAwait from './return-await'; import semi from './semi'; +import sortTypeConstituents from './sort-type-constituents'; import sortTypeUnionIntersectionMembers from './sort-type-union-intersection-members'; import spaceBeforeBlocks from './space-before-blocks'; import spaceBeforeFunctionParen from './space-before-function-paren'; @@ -245,6 +246,7 @@ export default { 'restrict-template-expressions': restrictTemplateExpressions, 'return-await': returnAwait, semi: semi, + 'sort-type-constituents': sortTypeConstituents, 'sort-type-union-intersection-members': sortTypeUnionIntersectionMembers, 'space-before-blocks': spaceBeforeBlocks, 'space-before-function-paren': spaceBeforeFunctionParen, diff --git a/packages/eslint-plugin/src/rules/sort-type-constituents.ts b/packages/eslint-plugin/src/rules/sort-type-constituents.ts new file mode 100644 index 000000000000..92b5e44c6f98 --- /dev/null +++ b/packages/eslint-plugin/src/rules/sort-type-constituents.ts @@ -0,0 +1,269 @@ +import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; +import { AST_NODE_TYPES } from '@typescript-eslint/utils'; + +import * as util from '../util'; +import { getEnumNames } from '../util'; + +enum Group { + conditional = 'conditional', + function = 'function', + import = 'import', + intersection = 'intersection', + keyword = 'keyword', + nullish = 'nullish', + literal = 'literal', + named = 'named', + object = 'object', + operator = 'operator', + tuple = 'tuple', + union = 'union', +} + +function getGroup(node: TSESTree.TypeNode): Group { + switch (node.type) { + case AST_NODE_TYPES.TSConditionalType: + return Group.conditional; + + case AST_NODE_TYPES.TSConstructorType: + case AST_NODE_TYPES.TSFunctionType: + return Group.function; + + case AST_NODE_TYPES.TSImportType: + return Group.import; + + case AST_NODE_TYPES.TSIntersectionType: + return Group.intersection; + + case AST_NODE_TYPES.TSAnyKeyword: + case AST_NODE_TYPES.TSBigIntKeyword: + case AST_NODE_TYPES.TSBooleanKeyword: + case AST_NODE_TYPES.TSNeverKeyword: + case AST_NODE_TYPES.TSNumberKeyword: + case AST_NODE_TYPES.TSObjectKeyword: + case AST_NODE_TYPES.TSStringKeyword: + case AST_NODE_TYPES.TSSymbolKeyword: + case AST_NODE_TYPES.TSThisType: + case AST_NODE_TYPES.TSUnknownKeyword: + case AST_NODE_TYPES.TSIntrinsicKeyword: + return Group.keyword; + + case AST_NODE_TYPES.TSNullKeyword: + case AST_NODE_TYPES.TSUndefinedKeyword: + case AST_NODE_TYPES.TSVoidKeyword: + return Group.nullish; + + case AST_NODE_TYPES.TSLiteralType: + case AST_NODE_TYPES.TSTemplateLiteralType: + return Group.literal; + + case AST_NODE_TYPES.TSArrayType: + case AST_NODE_TYPES.TSIndexedAccessType: + case AST_NODE_TYPES.TSInferType: + case AST_NODE_TYPES.TSTypeReference: + return Group.named; + + case AST_NODE_TYPES.TSMappedType: + case AST_NODE_TYPES.TSTypeLiteral: + return Group.object; + + case AST_NODE_TYPES.TSTypeOperator: + case AST_NODE_TYPES.TSTypeQuery: + return Group.operator; + + case AST_NODE_TYPES.TSTupleType: + return Group.tuple; + + case AST_NODE_TYPES.TSUnionType: + return Group.union; + + // These types should never occur as part of a union/intersection + case AST_NODE_TYPES.TSAbstractKeyword: + case AST_NODE_TYPES.TSAsyncKeyword: + case AST_NODE_TYPES.TSDeclareKeyword: + case AST_NODE_TYPES.TSExportKeyword: + case AST_NODE_TYPES.TSNamedTupleMember: + case AST_NODE_TYPES.TSOptionalType: + case AST_NODE_TYPES.TSPrivateKeyword: + case AST_NODE_TYPES.TSProtectedKeyword: + case AST_NODE_TYPES.TSPublicKeyword: + case AST_NODE_TYPES.TSReadonlyKeyword: + case AST_NODE_TYPES.TSRestType: + case AST_NODE_TYPES.TSStaticKeyword: + case AST_NODE_TYPES.TSTypePredicate: + /* istanbul ignore next */ + throw new Error(`Unexpected Type ${node.type}`); + } +} + +function requiresParentheses(node: TSESTree.TypeNode): boolean { + return ( + node.type === AST_NODE_TYPES.TSFunctionType || + node.type === AST_NODE_TYPES.TSConstructorType + ); +} + +export type Options = [ + { + checkIntersections?: boolean; + checkUnions?: boolean; + groupOrder?: string[]; + }, +]; +export type MessageIds = 'notSorted' | 'notSortedNamed' | 'suggestFix'; + +export default util.createRule({ + name: 'sort-type-constituents', + meta: { + type: 'suggestion', + docs: { + description: + 'Enforce constituents of a type union/intersection to be sorted alphabetically', + recommended: false, + }, + fixable: 'code', + hasSuggestions: true, + messages: { + notSorted: '{{type}} type constituents must be sorted.', + notSortedNamed: '{{type}} type {{name}} constituents must be sorted.', + suggestFix: 'Sort constituents of type (removes all comments).', + }, + schema: [ + { + type: 'object', + properties: { + checkIntersections: { + description: 'Whether to check intersection types.', + type: 'boolean', + }, + checkUnions: { + description: 'Whether to check union types.', + type: 'boolean', + }, + groupOrder: { + description: 'Ordering of the groups.', + type: 'array', + items: { + type: 'string', + enum: getEnumNames(Group), + }, + }, + }, + }, + ], + }, + defaultOptions: [ + { + checkIntersections: true, + checkUnions: true, + groupOrder: [ + Group.named, + Group.keyword, + Group.operator, + Group.literal, + Group.function, + Group.import, + Group.conditional, + Group.object, + Group.tuple, + Group.intersection, + Group.union, + Group.nullish, + ], + }, + ], + create(context, [{ checkIntersections, checkUnions, groupOrder }]) { + const sourceCode = context.getSourceCode(); + + const collator = new Intl.Collator('en', { + sensitivity: 'base', + numeric: true, + }); + + function checkSorting( + node: TSESTree.TSIntersectionType | TSESTree.TSUnionType, + ): void { + const sourceOrder = node.types.map(type => { + const group = groupOrder?.indexOf(getGroup(type)) ?? -1; + return { + group: group === -1 ? Number.MAX_SAFE_INTEGER : group, + node: type, + text: sourceCode.getText(type), + }; + }); + const expectedOrder = [...sourceOrder].sort((a, b) => { + if (a.group !== b.group) { + return a.group - b.group; + } + + return ( + collator.compare(a.text, b.text) || + (a.text < b.text ? -1 : a.text > b.text ? 1 : 0) + ); + }); + + const hasComments = node.types.some(type => { + const count = + sourceCode.getCommentsBefore(type).length + + sourceCode.getCommentsAfter(type).length; + return count > 0; + }); + + for (let i = 0; i < expectedOrder.length; i += 1) { + if (expectedOrder[i].node !== sourceOrder[i].node) { + let messageId: MessageIds = 'notSorted'; + const data = { + name: '', + type: + node.type === AST_NODE_TYPES.TSIntersectionType + ? 'Intersection' + : 'Union', + }; + if (node.parent?.type === AST_NODE_TYPES.TSTypeAliasDeclaration) { + messageId = 'notSortedNamed'; + data.name = node.parent.id.name; + } + + const fix: TSESLint.ReportFixFunction = fixer => { + const sorted = expectedOrder + .map(t => (requiresParentheses(t.node) ? `(${t.text})` : t.text)) + .join( + node.type === AST_NODE_TYPES.TSIntersectionType ? ' & ' : ' | ', + ); + + return fixer.replaceText(node, sorted); + }; + return context.report({ + node, + messageId, + data, + // don't autofix if any of the types have leading/trailing comments + // the logic for preserving them correctly is a pain - we may implement this later + ...(hasComments + ? { + suggest: [ + { + messageId: 'suggestFix', + fix, + }, + ], + } + : { fix }), + }); + } + } + } + + return { + ...(checkIntersections && { + TSIntersectionType(node): void { + checkSorting(node); + }, + }), + ...(checkUnions && { + TSUnionType(node): void { + checkSorting(node); + }, + }), + }; + }, +}); diff --git a/packages/eslint-plugin/src/rules/sort-type-union-intersection-members.ts b/packages/eslint-plugin/src/rules/sort-type-union-intersection-members.ts index ded7212c6c95..2a83b4b0525b 100644 --- a/packages/eslint-plugin/src/rules/sort-type-union-intersection-members.ts +++ b/packages/eslint-plugin/src/rules/sort-type-union-intersection-members.ts @@ -114,6 +114,7 @@ export type MessageIds = 'notSorted' | 'notSortedNamed' | 'suggestFix'; export default util.createRule({ name: 'sort-type-union-intersection-members', meta: { + deprecated: true, type: 'suggestion', docs: { description: @@ -127,6 +128,7 @@ export default util.createRule({ notSortedNamed: '{{type}} type {{name}} members must be sorted.', suggestFix: 'Sort members of type (removes all comments).', }, + replacedBy: ['@typescript-eslint/sort-type-constituents'], schema: [ { type: 'object', diff --git a/packages/eslint-plugin/tests/rules/sort-type-constituents.test.ts b/packages/eslint-plugin/tests/rules/sort-type-constituents.test.ts new file mode 100644 index 000000000000..b9989c2cf875 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/sort-type-constituents.test.ts @@ -0,0 +1,334 @@ +import type { TSESLint } from '@typescript-eslint/utils'; + +import type { + MessageIds, + Options, +} from '../../src/rules/sort-type-constituents'; +import rule from '../../src/rules/sort-type-constituents'; +import { noFormat, RuleTester } from '../RuleTester'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', +}); + +const valid = (operator: '|' | '&'): TSESLint.ValidTestCase[] => [ + { + code: `type T = A ${operator} B;`, + }, + { + code: `type T = A ${operator} /* comment */ B;`, + }, + { + code: `type T = 'A' ${operator} 'B';`, + }, + { + code: `type T = 1 ${operator} 2;`, + }, + { + code: noFormat`type T = (A) ${operator} (B);`, + }, + { + code: `type T = { a: string } ${operator} { b: string };`, + }, + { + code: `type T = [1, 2, 3] ${operator} [1, 2, 4];`, + }, + { + code: `type T = (() => string) ${operator} (() => void);`, + }, + { + code: `type T = () => string ${operator} void;`, + }, + { + // testing the default ordering + code: noFormat` +type T = + ${operator} A + ${operator} B + ${operator} intrinsic + ${operator} number[] + ${operator} string[] + ${operator} any + ${operator} string + ${operator} symbol + ${operator} this + ${operator} readonly number[] + ${operator} readonly string[] + ${operator} 'a' + ${operator} 'b' + ${operator} "a" + ${operator} "b" + ${operator} (() => string) + ${operator} (() => void) + ${operator} (new () => string) + ${operator} (new () => void) + ${operator} import('bar') + ${operator} import('foo') + ${operator} (number extends string ? unknown : never) + ${operator} (string extends string ? unknown : never) + ${operator} { [a in string]: string } + ${operator} { [a: string]: string } + ${operator} { [b in string]: string } + ${operator} { [b: string]: string } + ${operator} { a: string } + ${operator} { b: string } + ${operator} [1, 2, 3] + ${operator} [1, 2, 4] + ${operator} (A & B) + ${operator} (B & C) + ${operator} (A | B) + ${operator} (B | C) + ${operator} null + ${operator} undefined + `, + }, +]; +const invalid = ( + operator: '|' | '&', +): TSESLint.InvalidTestCase[] => { + const type = operator === '|' ? 'Union' : 'Intersection'; + return [ + { + code: `type T = B ${operator} A;`, + output: `type T = A ${operator} B;`, + errors: [ + { + messageId: 'notSortedNamed', + data: { + type, + name: 'T', + }, + }, + ], + }, + { + code: `type T = 'B' ${operator} 'A';`, + output: `type T = 'A' ${operator} 'B';`, + errors: [ + { + messageId: 'notSortedNamed', + data: { + type, + name: 'T', + }, + }, + ], + }, + { + code: `type T = 2 ${operator} 1;`, + output: `type T = 1 ${operator} 2;`, + errors: [ + { + messageId: 'notSortedNamed', + data: { + type, + name: 'T', + }, + }, + ], + }, + { + code: noFormat`type T = (B) ${operator} (A);`, + output: `type T = A ${operator} B;`, + errors: [ + { + messageId: 'notSortedNamed', + data: { + type, + name: 'T', + }, + }, + ], + }, + { + code: `type T = { b: string } ${operator} { a: string };`, + output: `type T = { a: string } ${operator} { b: string };`, + errors: [ + { + messageId: 'notSortedNamed', + data: { + type, + name: 'T', + }, + }, + ], + }, + { + code: `type T = [1, 2, 4] ${operator} [1, 2, 3];`, + output: `type T = [1, 2, 3] ${operator} [1, 2, 4];`, + errors: [ + { + messageId: 'notSortedNamed', + data: { + type, + name: 'T', + }, + }, + ], + }, + { + code: `type T = (() => void) ${operator} (() => string);`, + output: `type T = (() => string) ${operator} (() => void);`, + errors: [ + { + messageId: 'notSortedNamed', + data: { + type, + name: 'T', + }, + }, + ], + }, + { + code: `type T = () => void ${operator} string;`, + output: `type T = () => string ${operator} void;`, + errors: [ + { + messageId: 'notSorted', + data: { + type, + }, + }, + ], + }, + { + code: `type T = () => undefined ${operator} null;`, + output: `type T = () => null ${operator} undefined;`, + errors: [ + { + messageId: 'notSorted', + data: { + type, + }, + }, + ], + }, + { + code: noFormat` +type T = + ${operator} [1, 2, 4] + ${operator} [1, 2, 3] + ${operator} { b: string } + ${operator} { a: string } + ${operator} (() => void) + ${operator} (() => string) + ${operator} "b" + ${operator} "a" + ${operator} 'b' + ${operator} 'a' + ${operator} readonly string[] + ${operator} readonly number[] + ${operator} string[] + ${operator} number[] + ${operator} B + ${operator} A + ${operator} undefined + ${operator} null + ${operator} string + ${operator} any; + `, + output: ` +type T = + A ${operator} B ${operator} number[] ${operator} string[] ${operator} any ${operator} string ${operator} readonly number[] ${operator} readonly string[] ${operator} 'a' ${operator} 'b' ${operator} "a" ${operator} "b" ${operator} (() => string) ${operator} (() => void) ${operator} { a: string } ${operator} { b: string } ${operator} [1, 2, 3] ${operator} [1, 2, 4] ${operator} null ${operator} undefined; + `, + errors: [ + { + messageId: 'notSortedNamed', + data: { + type, + name: 'T', + }, + }, + ], + }, + { + code: `type T = B ${operator} /* comment */ A;`, + output: null, + errors: [ + { + messageId: 'notSortedNamed', + data: { + type, + name: 'T', + }, + suggestions: [ + { + messageId: 'suggestFix', + output: `type T = A ${operator} B;`, + }, + ], + }, + ], + }, + { + code: `type T = (() => /* comment */ A) ${operator} B;`, + output: `type T = B ${operator} (() => /* comment */ A);`, + errors: [ + { + messageId: 'notSortedNamed', + data: { + type, + name: 'T', + }, + suggestions: null, + }, + ], + }, + { + code: `type Expected = (new (x: number) => boolean) ${operator} string;`, + output: `type Expected = string ${operator} (new (x: number) => boolean);`, + errors: [ + { + messageId: 'notSortedNamed', + }, + ], + }, + ]; +}; + +ruleTester.run('sort-type-constituents', rule, { + valid: [ + ...valid('|'), + { + code: 'type T = B | A;', + options: [ + { + checkUnions: false, + }, + ], + }, + + ...valid('&'), + { + code: 'type T = B & A;', + options: [ + { + checkIntersections: false, + }, + ], + }, + + { + code: noFormat` +type T = [1] | 'a' | 'b' | "b" | 1 | 2 | {}; + `, + options: [ + { + groupOrder: ['tuple', 'literal', 'object'], + }, + ], + }, + { + // if not specified - groups should be placed last + code: ` +type T = 1 | string | {} | A; + `, + options: [ + { + groupOrder: ['literal', 'keyword'], + }, + ], + }, + ], + invalid: [...invalid('|'), ...invalid('&')], +}); From 344322add846d03c6c9981e486b09e6ba1196555 Mon Sep 17 00:00:00 2001 From: Shogo Hida Date: Thu, 27 Oct 2022 14:12:13 +0900 Subject: [PATCH 12/20] fix(eslint-plugin): enable react/jsx-curly-brace-presence lint rule in website package (#5894) Add 'react/jsx-curly-brace-presence': 'error' Co-authored-by: Josh Goldberg --- packages/website/.eslintrc.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/website/.eslintrc.js b/packages/website/.eslintrc.js index 2359a5a4cd5a..323ce34a38d7 100644 --- a/packages/website/.eslintrc.js +++ b/packages/website/.eslintrc.js @@ -23,6 +23,7 @@ module.exports = { 'react/jsx-no-target-blank': 'off', 'react/no-unescaped-entities': 'off', '@typescript-eslint/internal/prefer-ast-types-enum': 'off', + 'react/jsx-curly-brace-presence': 'error', }, settings: { react: { From d806bda82343712a24e3c78b9b34d4345dd1de3b Mon Sep 17 00:00:00 2001 From: kmin-jeong <53456037+kmin-jeong@users.noreply.github.com> Date: Sat, 29 Oct 2022 21:45:53 +0900 Subject: [PATCH 13/20] feat(eslint-plugin): [no-invalid-void-type] better report message for void used as a constituent inside a function return type (#5274) * feat: update repository * remove duplicate examples * feat:add about re-exporting --isolatedModules in When Not To Use It * feat: add exmaples * fix: remove duplicate examples in correct * fix:correct words * fix: fix space-between * fix: fix code * fix: fix code space * fix: check error * fix:missed examples * feat: read review and fix them fix examples and --Isolatedmodules explain * feat: add two taps one is correct, another is incorrect * fix: add * feat: fix explaination about `isolatedmodules` * fix: fix explain about `isolatedModules` * fis: modify When no to use it * fix: revert change log * fix: add lint * fix: add lint * add: fix code * fix:fix docs splits * feat: add lint * Update packages/eslint-plugin/docs/rules/consistent-type-exports.md * feat:change error message better * fix:separating message and made types about union * fix: separate error message * fix:fix mistake type * fix:fix typo * fix:fix error message in case of union * fix:fix error message in InvalidVoidForUnion * fix: add logic about union * feat: add test case * Switched change to just enhance error message * oops comment Co-authored-by: Josh Goldberg Co-authored-by: Josh Goldberg --- .../src/rules/no-invalid-void-type.ts | 23 +++++++++++----- .../tests/rules/no-invalid-void-type.test.ts | 26 ++++++++++++++++--- 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-invalid-void-type.ts b/packages/eslint-plugin/src/rules/no-invalid-void-type.ts index 7cda184e4fdc..9f938ac438f7 100644 --- a/packages/eslint-plugin/src/rules/no-invalid-void-type.ts +++ b/packages/eslint-plugin/src/rules/no-invalid-void-type.ts @@ -13,7 +13,8 @@ type MessageIds = | 'invalidVoidNotReturnOrGeneric' | 'invalidVoidNotReturn' | 'invalidVoidNotReturnOrThisParam' - | 'invalidVoidNotReturnOrThisParamOrGeneric'; + | 'invalidVoidNotReturnOrThisParamOrGeneric' + | 'invalidVoidUnionConstituent'; export default util.createRule<[Options], MessageIds>({ name: 'no-invalid-void-type', @@ -25,14 +26,16 @@ export default util.createRule<[Options], MessageIds>({ }, messages: { invalidVoidForGeneric: - '{{ generic }} may not have void as a type variable.', + '{{ generic }} may not have void as a type argument.', invalidVoidNotReturnOrGeneric: - 'void is only valid as a return type or generic type variable.', + 'void is only valid as a return type or generic type argument.', invalidVoidNotReturn: 'void is only valid as a return type.', invalidVoidNotReturnOrThisParam: 'void is only valid as return type or type of `this` parameter.', invalidVoidNotReturnOrThisParamOrGeneric: - 'void is only valid as a return type or generic type variable or the type of a `this` parameter.', + 'void is only valid as a return type or generic type argument or the type of a `this` parameter.', + invalidVoidUnionConstituent: + 'void is not valid as a constituent in a union type', }, schema: [ { @@ -136,7 +139,7 @@ export default util.createRule<[Options], MessageIds>({ ): void { if (parentNode.default !== node) { context.report({ - messageId: 'invalidVoidNotReturnOrGeneric', + messageId: getNotReturnOrGenericMessageId(node), node, }); } @@ -218,7 +221,7 @@ export default util.createRule<[Options], MessageIds>({ allowInGenericTypeArguments && allowAsThisParameter ? 'invalidVoidNotReturnOrThisParamOrGeneric' : allowInGenericTypeArguments - ? 'invalidVoidNotReturnOrGeneric' + ? getNotReturnOrGenericMessageId(node) : allowAsThisParameter ? 'invalidVoidNotReturnOrThisParam' : 'invalidVoidNotReturn', @@ -228,3 +231,11 @@ export default util.createRule<[Options], MessageIds>({ }; }, }); + +function getNotReturnOrGenericMessageId( + node: TSESTree.TSVoidKeyword, +): MessageIds { + return node.parent!.type === AST_NODE_TYPES.TSUnionType + ? 'invalidVoidUnionConstituent' + : 'invalidVoidNotReturnOrGeneric'; +} diff --git a/packages/eslint-plugin/tests/rules/no-invalid-void-type.test.ts b/packages/eslint-plugin/tests/rules/no-invalid-void-type.test.ts index 8164e85b2bd2..1a972b665be5 100644 --- a/packages/eslint-plugin/tests/rules/no-invalid-void-type.test.ts +++ b/packages/eslint-plugin/tests/rules/no-invalid-void-type.test.ts @@ -316,7 +316,7 @@ ruleTester.run('allowInGenericTypeArguments: true', rule, { code: 'type UnionType2 = string | number | void;', errors: [ { - messageId: 'invalidVoidNotReturnOrGeneric', + messageId: 'invalidVoidUnionConstituent', line: 1, column: 37, }, @@ -326,12 +326,32 @@ ruleTester.run('allowInGenericTypeArguments: true', rule, { code: 'type UnionType3 = string | ((number & any) | (string | void));', errors: [ { - messageId: 'invalidVoidNotReturnOrGeneric', + messageId: 'invalidVoidUnionConstituent', line: 1, column: 56, }, ], }, + { + code: 'declare function test(): number | void;', + errors: [ + { + messageId: 'invalidVoidUnionConstituent', + line: 1, + column: 35, + }, + ], + }, + { + code: 'declare function test(): T;', + errors: [ + { + messageId: 'invalidVoidUnionConstituent', + line: 1, + column: 42, + }, + ], + }, { code: 'type IntersectionType = string & number & void;', errors: [ @@ -394,7 +414,7 @@ ruleTester.run('allowInGenericTypeArguments: true', rule, { code: 'type invalidVoidUnion = void | Map;', errors: [ { - messageId: 'invalidVoidNotReturnOrGeneric', + messageId: 'invalidVoidUnionConstituent', line: 1, column: 25, }, From 891b0879ba9c64a4722b8c0bf9e599a725b6d6df Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Sat, 29 Oct 2022 06:25:11 -0700 Subject: [PATCH 14/20] fix(typescript-estree): don't allow single-run unless we're in type-aware linting mode (#5893) Co-authored-by: Josh Goldberg --- .../src/parseSettings/inferSingleRun.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/typescript-estree/src/parseSettings/inferSingleRun.ts b/packages/typescript-estree/src/parseSettings/inferSingleRun.ts index 723f857ece96..5d765a22ce95 100644 --- a/packages/typescript-estree/src/parseSettings/inferSingleRun.ts +++ b/packages/typescript-estree/src/parseSettings/inferSingleRun.ts @@ -15,6 +15,16 @@ import type { TSESTreeOptions } from '../parser-options'; * @returns Whether this is part of a single run, rather than a long-running process. */ export function inferSingleRun(options: TSESTreeOptions | undefined): boolean { + if ( + // single-run implies type-aware linting - no projects means we can't be in single-run mode + options?.project == null || + // programs passed via options means the user should be managing the programs, so we shouldn't + // be creating our own single-run programs accidentally + options?.programs != null + ) { + return false; + } + // Allow users to explicitly inform us of their intent to perform a single run (or not) with TSESTREE_SINGLE_RUN if (process.env.TSESTREE_SINGLE_RUN === 'false') { return false; From 8ed72192c274249d26628fb125796e71318b857a Mon Sep 17 00:00:00 2001 From: YeonJuan Date: Sun, 30 Oct 2022 12:25:18 +0900 Subject: [PATCH 15/20] fix(eslint-plugin): [no-extra-parens] handle type assertion in extends clause (#5901) * fix(eslint-plugin): [no-extra-parens] handle type assertion in extends clause * Add test cases * Handle class expression * Add test cases --- .../src/rules/no-extra-parens.ts | 26 +++++++++++++-- .../tests/rules/no-extra-parens.test.ts | 32 +++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-extra-parens.ts b/packages/eslint-plugin/src/rules/no-extra-parens.ts index 3a4a5973074d..a44276a0a761 100644 --- a/packages/eslint-plugin/src/rules/no-extra-parens.ts +++ b/packages/eslint-plugin/src/rules/no-extra-parens.ts @@ -141,8 +141,30 @@ export default util.createRule({ }, BinaryExpression: binaryExp, CallExpression: callExp, - // ClassDeclaration - // ClassExpression + ClassDeclaration(node) { + if (node.superClass?.type === AST_NODE_TYPES.TSAsExpression) { + return rules.ClassDeclaration({ + ...node, + superClass: { + ...node.superClass, + type: AST_NODE_TYPES.SequenceExpression as any, + }, + }); + } + return rules.ClassDeclaration(node); + }, + ClassExpression(node) { + if (node.superClass?.type === AST_NODE_TYPES.TSAsExpression) { + return rules.ClassExpression({ + ...node, + superClass: { + ...node.superClass, + type: AST_NODE_TYPES.SequenceExpression as any, + }, + }); + } + return rules.ClassExpression(node); + }, ConditionalExpression(node) { // reduces the precedence of the node so the rule thinks it needs to be wrapped if (util.isTypeAssertion(node.test)) { diff --git a/packages/eslint-plugin/tests/rules/no-extra-parens.test.ts b/packages/eslint-plugin/tests/rules/no-extra-parens.test.ts index 6a9e87111aa0..369f55101f28 100644 --- a/packages/eslint-plugin/tests/rules/no-extra-parens.test.ts +++ b/packages/eslint-plugin/tests/rules/no-extra-parens.test.ts @@ -141,6 +141,10 @@ t.true((me.get as SinonStub).calledWithExactly('/foo', other)); t.true((me.get).calledWithExactly('/foo', other)); (requestInit.headers as Headers).get('Cookie'); ( requestInit.headers).get('Cookie'); +class Foo {} +class Foo extends (Bar as any) {} +const foo = class {}; +const foo = class extends (Bar as any) {} `, parserOptions: { ecmaFeatures: { @@ -254,6 +258,10 @@ new a((1)); a<(A)>((1)); async function f(arg: Promise) { await (arg); } async function f(arg: any) { await ((arg as Promise)); } +class Foo extends ((Bar as any)) {} +class Foo extends (Bar) {} +const foo = class extends ((Bar as any)) {} +const foo = class extends (Bar) {} `, output: ` a = b * c; @@ -267,6 +275,10 @@ new a(1); a<(A)>(1); async function f(arg: Promise) { await arg; } async function f(arg: any) { await (arg as Promise); } +class Foo extends (Bar as any) {} +class Foo extends Bar {} +const foo = class extends (Bar as any) {} +const foo = class extends Bar {} `, errors: [ { @@ -324,6 +336,26 @@ async function f(arg: any) { await (arg as Promise); } line: 12, column: 37, }, + { + messageId: 'unexpected', + line: 13, + column: 20, + }, + { + messageId: 'unexpected', + line: 14, + column: 19, + }, + { + messageId: 'unexpected', + line: 15, + column: 28, + }, + { + messageId: 'unexpected', + line: 16, + column: 27, + }, ], }), ...batchedSingleLineTests({ From 54141946ac2a25799bb622d47d9a3fd1fb316ef9 Mon Sep 17 00:00:00 2001 From: "typescript-eslint[bot]" <53356952+typescript-eslint[bot]@users.noreply.github.com> Date: Sun, 30 Oct 2022 23:03:00 -0400 Subject: [PATCH 16/20] chore: update sponsors (#5905) Co-authored-by: typescript-eslint[bot] --- packages/website/data/sponsors.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/website/data/sponsors.json b/packages/website/data/sponsors.json index faef97d207fa..a4aa0623e042 100644 --- a/packages/website/data/sponsors.json +++ b/packages/website/data/sponsors.json @@ -39,6 +39,14 @@ "totalDonations": 120000, "website": "https://blog.coinbase.com/engineering-and-security/home" }, + { + "id": "Sentry", + "image": "https://images.opencollective.com/sentry/9620d33/logo.png", + "name": "Sentry", + "tier": "sponsor", + "totalDonations": 114800, + "website": "https://sentry.io/welcome/" + }, { "id": "n8n.io - n8n GmbH", "image": "https://images.opencollective.com/n8n/dca2f0c/logo.png", @@ -79,14 +87,6 @@ "totalDonations": 54000, "website": "https://www.future-processing.com/" }, - { - "id": "Sentry", - "image": "https://images.opencollective.com/sentry/9620d33/logo.png", - "name": "Sentry", - "tier": "supporter", - "totalDonations": 50000, - "website": "https://sentry.io/welcome/" - }, { "id": "Whitebox", "image": "https://images.opencollective.com/whiteboxinc/ef0d11d/logo.png", From 1f14c03d2696ce02537c08aba683bdd587de5e6b Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Mon, 31 Oct 2022 03:53:04 -0700 Subject: [PATCH 17/20] docs(eslint-plugin): [consistent-type-imports] make a note about `parserOptions.emitDecoratorMetadata` (#5904) * docs(eslint-plugin): [consistent-type-imports] make a note about `parserOptions.emitDecoratorMetadata` * Update consistent-type-imports.md * Update consistent-type-imports.md * Update consistent-type-imports.md * Update consistent-type-imports.md --- .../eslint-plugin/docs/rules/consistent-type-imports.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/eslint-plugin/docs/rules/consistent-type-imports.md b/packages/eslint-plugin/docs/rules/consistent-type-imports.md index 6e6912d34cf2..745930d0d54c 100644 --- a/packages/eslint-plugin/docs/rules/consistent-type-imports.md +++ b/packages/eslint-plugin/docs/rules/consistent-type-imports.md @@ -48,6 +48,12 @@ type T = import('Foo').Foo; const x: import('Bar') = 1; ``` +## Usage with `emitDecoratorMetadata` + +The `emitDecoratorMetadata` compiler option changes the code the TypeScript emits. In short - it causes TypeScript to create references to value imports when they are used in a type-only location. If you are using `emitDecoratorMetadata` then our tooling will require additional information in order for the rule to work correctly. + +If you are using [type-aware linting](https://typescript-eslint.io/docs/linting/typed-linting), then you just need to ensure that the `tsconfig.json` you've configured for `parserOptions.project` has `emitDecoratorMetadata` turned on. Otherwise you can explicitly tell our tooling to analyze your code as if the compiler option was turned on [by setting `parserOptions.emitDecoratorMetadata` to `true`](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/README.md#parseroptionsemitdecoratormetadata). + ## When Not To Use It - If you specifically want to use both import kinds for stylistic reasons, you can disable this rule. From 0520d53536af411d66ce2ce0dd478365e67adbac Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Mon, 31 Oct 2022 05:03:38 -0700 Subject: [PATCH 18/20] feat(utils): add `RuleTester` API for top-level dependency constraints (#5896) * feat(utils): add `RuleTester` API for top-level dependency constraints * Apply suggestions from code review Co-authored-by: Josh Goldberg * address comments * oops Co-authored-by: Josh Goldberg --- .../eslint-utils/rule-tester/RuleTester.ts | 89 +++++++++++++++--- packages/utils/src/ts-eslint/RuleTester.ts | 21 ++--- .../rule-tester/RuleTester.test.ts | 91 ++++++++++++++++++- 3 files changed, 175 insertions(+), 26 deletions(-) diff --git a/packages/utils/src/eslint-utils/rule-tester/RuleTester.ts b/packages/utils/src/eslint-utils/rule-tester/RuleTester.ts index e81d23d02065..54a645ccf25a 100644 --- a/packages/utils/src/eslint-utils/rule-tester/RuleTester.ts +++ b/packages/utils/src/eslint-utils/rule-tester/RuleTester.ts @@ -1,9 +1,13 @@ import type * as TSESLintParserType from '@typescript-eslint/parser'; +import assert from 'assert'; import { version as eslintVersion } from 'eslint/package.json'; import * as path from 'path'; import * as semver from 'semver'; -import * as TSESLint from '../../ts-eslint'; +import type { ParserOptions } from '../../ts-eslint/ParserOptions'; +import type { RuleModule } from '../../ts-eslint/Rule'; +import type { RuleTesterTestFrameworkFunction } from '../../ts-eslint/RuleTester'; +import * as BaseRuleTester from '../../ts-eslint/RuleTester'; import { deepMerge } from '../deepMerge'; import type { DependencyConstraint } from './dependencyConstraints'; import { satisfiesAllDependencyConstraints } from './dependencyConstraints'; @@ -11,18 +15,28 @@ import { satisfiesAllDependencyConstraints } from './dependencyConstraints'; const TS_ESLINT_PARSER = '@typescript-eslint/parser'; const ERROR_MESSAGE = `Do not set the parser at the test level unless you want to use a parser other than ${TS_ESLINT_PARSER}`; -type RuleTesterConfig = Omit & { +type RuleTesterConfig = Omit & { parser: typeof TS_ESLINT_PARSER; + /** + * Constraints that must pass in the current environment for any tests to run + */ + dependencyConstraints?: DependencyConstraint; }; interface InvalidTestCase< TMessageIds extends string, TOptions extends Readonly, -> extends TSESLint.InvalidTestCase { +> extends BaseRuleTester.InvalidTestCase { + /** + * Constraints that must pass in the current environment for the test to run + */ dependencyConstraints?: DependencyConstraint; } interface ValidTestCase> - extends TSESLint.ValidTestCase { + extends BaseRuleTester.ValidTestCase { + /** + * Constraints that must pass in the current environment for the test to run + */ dependencyConstraints?: DependencyConstraint; } interface RunTests< @@ -36,24 +50,42 @@ interface RunTests< type AfterAll = (fn: () => void) => void; -class RuleTester extends TSESLint.RuleTester { +function isDescribeWithSkip( + value: unknown, +): value is RuleTesterTestFrameworkFunction & { + skip: RuleTesterTestFrameworkFunction; +} { + return ( + typeof value === 'object' && + value != null && + 'skip' in value && + typeof (value as Record).skip === 'function' + ); +} + +class RuleTester extends BaseRuleTester.RuleTester { readonly #baseOptions: RuleTesterConfig; - static #afterAll: AfterAll; + static #afterAll: AfterAll | undefined; /** * If you supply a value to this property, the rule tester will call this instead of using the version defined on * the global namespace. */ static get afterAll(): AfterAll { return ( - this.#afterAll || + this.#afterAll ?? (typeof afterAll === 'function' ? afterAll : (): void => {}) ); } - static set afterAll(value) { + static set afterAll(value: AfterAll | undefined) { this.#afterAll = value; } + private get staticThis(): typeof RuleTester { + // the cast here is due to https://github.com/microsoft/TypeScript/issues/3841 + return this.constructor as typeof RuleTester; + } + constructor(baseOptions: RuleTesterConfig) { super({ ...baseOptions, @@ -73,8 +105,7 @@ class RuleTester extends TSESLint.RuleTester { // make sure that the parser doesn't hold onto file handles between tests // on linux (i.e. our CI env), there can be very a limited number of watch handles available - // the cast here is due to https://github.com/microsoft/TypeScript/issues/3841 - (this.constructor as typeof RuleTester).afterAll(() => { + this.staticThis.afterAll(() => { try { // instead of creating a hard dependency, just use a soft require // a bit weird, but if they're using this tooling, it'll be installed @@ -85,11 +116,11 @@ class RuleTester extends TSESLint.RuleTester { } }); } - private getFilename(testOptions?: TSESLint.ParserOptions): string { + private getFilename(testOptions?: ParserOptions): string { const resolvedOptions = deepMerge( this.#baseOptions.parserOptions, testOptions, - ) as TSESLint.ParserOptions; + ) as ParserOptions; const filename = `file.ts${resolvedOptions.ecmaFeatures?.jsx ? 'x' : ''}`; if (resolvedOptions.project) { return path.join( @@ -107,9 +138,41 @@ class RuleTester extends TSESLint.RuleTester { // This is a lot more explicit run>( name: string, - rule: TSESLint.RuleModule, + rule: RuleModule, testsReadonly: RunTests, ): void { + if ( + this.#baseOptions.dependencyConstraints && + !satisfiesAllDependencyConstraints( + this.#baseOptions.dependencyConstraints, + ) + ) { + if (isDescribeWithSkip(this.staticThis.describe)) { + // for frameworks like mocha or jest that have a "skip" version of their function + // we can provide a nice skipped test! + this.staticThis.describe.skip(name, () => { + this.staticThis.it( + 'All tests skipped due to unsatisfied constructor dependency constraints', + () => {}, + ); + }); + } else { + // otherwise just declare an empty test + this.staticThis.describe(name, () => { + this.staticThis.it( + 'All tests skipped due to unsatisfied constructor dependency constraints', + () => { + // some frameworks error if there are no assertions + assert.equal(true, true); + }, + ); + }); + } + + // don't run any tests because we don't match the base constraint + return; + } + const tests = { // standardize the valid tests as objects valid: testsReadonly.valid.map(test => { diff --git a/packages/utils/src/ts-eslint/RuleTester.ts b/packages/utils/src/ts-eslint/RuleTester.ts index 7002fc538cd5..6c0b98b795f2 100644 --- a/packages/utils/src/ts-eslint/RuleTester.ts +++ b/packages/utils/src/ts-eslint/RuleTester.ts @@ -125,6 +125,10 @@ interface TestCaseError { // readonly message?: string | RegExp; } +/** + * @param text a string describing the rule + * @param callback the test callback + */ type RuleTesterTestFrameworkFunction = ( text: string, callback: () => void, @@ -166,31 +170,26 @@ declare class RuleTesterBase { /** * If you supply a value to this property, the rule tester will call this instead of using the version defined on * the global namespace. - * @param text a string describing the rule - * @param callback the test callback */ - static describe?: RuleTesterTestFrameworkFunction; + static get describe(): RuleTesterTestFrameworkFunction; + static set describe(value: RuleTesterTestFrameworkFunction | undefined); /** * If you supply a value to this property, the rule tester will call this instead of using the version defined on * the global namespace. - * @param text a string describing the test case - * @param callback the test callback */ - static it?: RuleTesterTestFrameworkFunction; + static get it(): RuleTesterTestFrameworkFunction; + static set it(value: RuleTesterTestFrameworkFunction | undefined); /** * If you supply a value to this property, the rule tester will call this instead of using the version defined on * the global namespace. - * @param text a string describing the test case - * @param callback the test callback */ - static itOnly?: RuleTesterTestFrameworkFunction; + static get itOnly(): RuleTesterTestFrameworkFunction; + static set itOnly(value: RuleTesterTestFrameworkFunction | undefined); /** * Define a rule for one particular run of tests. - * @param name The name of the rule to define. - * @param rule The rule definition. */ defineRule>( name: string, diff --git a/packages/utils/tests/eslint-utils/rule-tester/RuleTester.test.ts b/packages/utils/tests/eslint-utils/rule-tester/RuleTester.test.ts index cab7666196ab..2e6203329429 100644 --- a/packages/utils/tests/eslint-utils/rule-tester/RuleTester.test.ts +++ b/packages/utils/tests/eslint-utils/rule-tester/RuleTester.test.ts @@ -73,8 +73,8 @@ RuleTester.itOnly = jest.fn(); /* eslint-enable jest/prefer-spy-on */ const mockedAfterAll = jest.mocked(RuleTester.afterAll); -const _mockedDescribe = jest.mocked(RuleTester.describe); -const _mockedIt = jest.mocked(RuleTester.it); +const mockedDescribe = jest.mocked(RuleTester.describe); +const mockedIt = jest.mocked(RuleTester.it); const _mockedItOnly = jest.mocked(RuleTester.itOnly); const runSpy = jest.spyOn(BaseRuleTester.prototype, 'run'); const mockedParserClearCaches = jest.mocked(parser.clearCaches); @@ -715,5 +715,92 @@ describe('RuleTester', () => { } `); }); + + describe('constructor constraints', () => { + it('skips all tests if a constructor constraint is not satisifed', () => { + const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', + dependencyConstraints: { + 'totally-real-dependency': '999', + }, + }); + + ruleTester.run('my-rule', NOOP_RULE, { + invalid: [ + { + code: 'failing - major', + errors: [], + }, + ], + valid: [ + { + code: 'passing - major', + }, + ], + }); + + // trigger the describe block + expect(mockedDescribe.mock.calls.length).toBeGreaterThanOrEqual(1); + mockedDescribe.mock.lastCall?.[1](); + expect(mockedDescribe.mock.calls).toMatchInlineSnapshot(` + [ + [ + "my-rule", + [Function], + ], + ] + `); + expect(mockedIt.mock.lastCall).toMatchInlineSnapshot(` + [ + "All tests skipped due to unsatisfied constructor dependency constraints", + [Function], + ] + `); + }); + + it('does not skip all tests if a constructor constraint is satisifed', () => { + const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', + dependencyConstraints: { + 'totally-real-dependency': '10', + }, + }); + + ruleTester.run('my-rule', NOOP_RULE, { + invalid: [ + { + code: 'valid', + errors: [], + }, + ], + valid: [ + { + code: 'valid', + }, + ], + }); + + // trigger the describe block + expect(mockedDescribe.mock.calls.length).toBeGreaterThanOrEqual(1); + mockedDescribe.mock.lastCall?.[1](); + expect(mockedDescribe.mock.calls).toMatchInlineSnapshot(` + [ + [ + "my-rule", + [Function], + ], + [ + "valid", + [Function], + ], + [ + "invalid", + [Function], + ], + ] + `); + // expect(mockedIt.mock.lastCall).toMatchInlineSnapshot(`undefined`); + }); + }); }); }); From 5c316c12f09d58aee6ee634a8055533f361f1589 Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Mon, 31 Oct 2022 05:58:30 -0700 Subject: [PATCH 19/20] fix(ast-spec): add TSQualifiedName to TypeNode union (#5906) * fix(ast-spec): add TSQualifiedName to TypeNode union * fix rule with missing type --- packages/ast-spec/src/unions/TypeNode.ts | 2 ++ packages/eslint-plugin/src/rules/sort-type-constituents.ts | 1 + .../src/rules/sort-type-union-intersection-members.ts | 1 + .../tests/rules/sort-type-constituents.test.ts | 6 +++++- .../rules/sort-type-union-intersection-members.test.ts | 6 +++++- 5 files changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/ast-spec/src/unions/TypeNode.ts b/packages/ast-spec/src/unions/TypeNode.ts index 461281cd4ce3..f1859d7d3478 100644 --- a/packages/ast-spec/src/unions/TypeNode.ts +++ b/packages/ast-spec/src/unions/TypeNode.ts @@ -25,6 +25,7 @@ import type { TSOptionalType } from '../type/TSOptionalType/spec'; import type { TSPrivateKeyword } from '../type/TSPrivateKeyword/spec'; import type { TSProtectedKeyword } from '../type/TSProtectedKeyword/spec'; import type { TSPublicKeyword } from '../type/TSPublicKeyword/spec'; +import type { TSQualifiedName } from '../type/TSQualifiedName/spec'; import type { TSReadonlyKeyword } from '../type/TSReadonlyKeyword/spec'; import type { TSRestType } from '../type/TSRestType/spec'; import type { TSStaticKeyword } from '../type/TSStaticKeyword/spec'; @@ -71,6 +72,7 @@ export type TypeNode = | TSPrivateKeyword | TSProtectedKeyword | TSPublicKeyword + | TSQualifiedName | TSReadonlyKeyword | TSRestType | TSStaticKeyword diff --git a/packages/eslint-plugin/src/rules/sort-type-constituents.ts b/packages/eslint-plugin/src/rules/sort-type-constituents.ts index 92b5e44c6f98..1abeddcf8231 100644 --- a/packages/eslint-plugin/src/rules/sort-type-constituents.ts +++ b/packages/eslint-plugin/src/rules/sort-type-constituents.ts @@ -60,6 +60,7 @@ function getGroup(node: TSESTree.TypeNode): Group { case AST_NODE_TYPES.TSIndexedAccessType: case AST_NODE_TYPES.TSInferType: case AST_NODE_TYPES.TSTypeReference: + case AST_NODE_TYPES.TSQualifiedName: return Group.named; case AST_NODE_TYPES.TSMappedType: diff --git a/packages/eslint-plugin/src/rules/sort-type-union-intersection-members.ts b/packages/eslint-plugin/src/rules/sort-type-union-intersection-members.ts index 2a83b4b0525b..1fbf91b9ae89 100644 --- a/packages/eslint-plugin/src/rules/sort-type-union-intersection-members.ts +++ b/packages/eslint-plugin/src/rules/sort-type-union-intersection-members.ts @@ -60,6 +60,7 @@ function getGroup(node: TSESTree.TypeNode): Group { case AST_NODE_TYPES.TSIndexedAccessType: case AST_NODE_TYPES.TSInferType: case AST_NODE_TYPES.TSTypeReference: + case AST_NODE_TYPES.TSQualifiedName: return Group.named; case AST_NODE_TYPES.TSMappedType: diff --git a/packages/eslint-plugin/tests/rules/sort-type-constituents.test.ts b/packages/eslint-plugin/tests/rules/sort-type-constituents.test.ts index b9989c2cf875..2587375060ed 100644 --- a/packages/eslint-plugin/tests/rules/sort-type-constituents.test.ts +++ b/packages/eslint-plugin/tests/rules/sort-type-constituents.test.ts @@ -45,6 +45,8 @@ const valid = (operator: '|' | '&'): TSESLint.ValidTestCase[] => [ type T = ${operator} A ${operator} B + ${operator} C.D + ${operator} D.E ${operator} intrinsic ${operator} number[] ${operator} string[] @@ -220,6 +222,8 @@ type T = ${operator} readonly number[] ${operator} string[] ${operator} number[] + ${operator} D.E + ${operator} C.D ${operator} B ${operator} A ${operator} undefined @@ -229,7 +233,7 @@ type T = `, output: ` type T = - A ${operator} B ${operator} number[] ${operator} string[] ${operator} any ${operator} string ${operator} readonly number[] ${operator} readonly string[] ${operator} 'a' ${operator} 'b' ${operator} "a" ${operator} "b" ${operator} (() => string) ${operator} (() => void) ${operator} { a: string } ${operator} { b: string } ${operator} [1, 2, 3] ${operator} [1, 2, 4] ${operator} null ${operator} undefined; + A ${operator} B ${operator} C.D ${operator} D.E ${operator} number[] ${operator} string[] ${operator} any ${operator} string ${operator} readonly number[] ${operator} readonly string[] ${operator} 'a' ${operator} 'b' ${operator} "a" ${operator} "b" ${operator} (() => string) ${operator} (() => void) ${operator} { a: string } ${operator} { b: string } ${operator} [1, 2, 3] ${operator} [1, 2, 4] ${operator} null ${operator} undefined; `, errors: [ { diff --git a/packages/eslint-plugin/tests/rules/sort-type-union-intersection-members.test.ts b/packages/eslint-plugin/tests/rules/sort-type-union-intersection-members.test.ts index 38ae67f18043..a24959d8b6a6 100644 --- a/packages/eslint-plugin/tests/rules/sort-type-union-intersection-members.test.ts +++ b/packages/eslint-plugin/tests/rules/sort-type-union-intersection-members.test.ts @@ -45,6 +45,8 @@ const valid = (operator: '|' | '&'): TSESLint.ValidTestCase[] => [ type T = ${operator} A ${operator} B + ${operator} C.D + ${operator} D.E ${operator} intrinsic ${operator} number[] ${operator} string[] @@ -220,6 +222,8 @@ type T = ${operator} readonly number[] ${operator} string[] ${operator} number[] + ${operator} D.E + ${operator} C.D ${operator} B ${operator} A ${operator} undefined @@ -229,7 +233,7 @@ type T = `, output: ` type T = - A ${operator} B ${operator} number[] ${operator} string[] ${operator} any ${operator} string ${operator} readonly number[] ${operator} readonly string[] ${operator} 'a' ${operator} 'b' ${operator} "a" ${operator} "b" ${operator} (() => string) ${operator} (() => void) ${operator} { a: string } ${operator} { b: string } ${operator} [1, 2, 3] ${operator} [1, 2, 4] ${operator} null ${operator} undefined; + A ${operator} B ${operator} C.D ${operator} D.E ${operator} number[] ${operator} string[] ${operator} any ${operator} string ${operator} readonly number[] ${operator} readonly string[] ${operator} 'a' ${operator} 'b' ${operator} "a" ${operator} "b" ${operator} (() => string) ${operator} (() => void) ${operator} { a: string } ${operator} { b: string } ${operator} [1, 2, 3] ${operator} [1, 2, 4] ${operator} null ${operator} undefined; `, errors: [ { From 1e5e9ea4cac25947c3a8748647a4fb4d329c4b25 Mon Sep 17 00:00:00 2001 From: "typescript-eslint[bot]" Date: Mon, 31 Oct 2022 17:35:31 +0000 Subject: [PATCH 20/20] chore: publish v5.42.0 --- CHANGELOG.md | 22 ++++++++++++++++++++ lerna.json | 2 +- packages/ast-spec/CHANGELOG.md | 6 ++++++ packages/ast-spec/package.json | 2 +- packages/eslint-plugin-internal/CHANGELOG.md | 4 ++++ packages/eslint-plugin-internal/package.json | 6 +++--- packages/eslint-plugin-tslint/CHANGELOG.md | 4 ++++ packages/eslint-plugin-tslint/package.json | 6 +++--- packages/eslint-plugin/CHANGELOG.md | 12 +++++++++++ packages/eslint-plugin/package.json | 8 +++---- packages/experimental-utils/CHANGELOG.md | 4 ++++ packages/experimental-utils/package.json | 4 ++-- packages/parser/CHANGELOG.md | 10 +++++++++ packages/parser/package.json | 8 +++---- packages/scope-manager/CHANGELOG.md | 10 +++++++++ packages/scope-manager/package.json | 8 +++---- packages/shared-fixtures/CHANGELOG.md | 4 ++++ packages/shared-fixtures/package.json | 2 +- packages/type-utils/CHANGELOG.md | 4 ++++ packages/type-utils/package.json | 8 +++---- packages/types/CHANGELOG.md | 4 ++++ packages/types/package.json | 2 +- packages/typescript-estree/CHANGELOG.md | 10 +++++++++ packages/typescript-estree/package.json | 8 +++---- packages/utils/CHANGELOG.md | 6 ++++++ packages/utils/package.json | 10 ++++----- packages/visitor-keys/CHANGELOG.md | 4 ++++ packages/visitor-keys/package.json | 4 ++-- packages/website-eslint/CHANGELOG.md | 4 ++++ packages/website-eslint/package.json | 16 +++++++------- packages/website/CHANGELOG.md | 15 +++++++++++++ packages/website/package.json | 8 +++---- 32 files changed, 174 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0304329f3aa9..f4685ce8dae9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,28 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [5.42.0](https://github.com/typescript-eslint/typescript-eslint/compare/v5.41.0...v5.42.0) (2022-10-31) + +### Bug Fixes + +- **ast-spec:** add TSQualifiedName to TypeNode union ([#5906](https://github.com/typescript-eslint/typescript-eslint/issues/5906)) ([5c316c1](https://github.com/typescript-eslint/typescript-eslint/commit/5c316c12f09d58aee6ee634a8055533f361f1589)) +- **eslint-plugin:** [no-extra-parens] handle type assertion in extends clause ([#5901](https://github.com/typescript-eslint/typescript-eslint/issues/5901)) ([8ed7219](https://github.com/typescript-eslint/typescript-eslint/commit/8ed72192c274249d26628fb125796e71318b857a)) +- **eslint-plugin:** enable react/jsx-curly-brace-presence lint rule in website package ([#5894](https://github.com/typescript-eslint/typescript-eslint/issues/5894)) ([344322a](https://github.com/typescript-eslint/typescript-eslint/commit/344322add846d03c6c9981e486b09e6ba1196555)) +- **typescript-estree:** don't allow single-run unless we're in type-aware linting mode ([#5893](https://github.com/typescript-eslint/typescript-eslint/issues/5893)) ([891b087](https://github.com/typescript-eslint/typescript-eslint/commit/891b0879ba9c64a4722b8c0bf9e599a725b6d6df)) + +### Features + +- **eslint-plugin:** [member-ordering] add natural sort order ([#5662](https://github.com/typescript-eslint/typescript-eslint/issues/5662)) ([1eaae09](https://github.com/typescript-eslint/typescript-eslint/commit/1eaae09ecca359f366b94f6a04665403f48b05c7)) +- **eslint-plugin:** [no-invalid-void-type] better report message for void used as a constituent inside a function return type ([#5274](https://github.com/typescript-eslint/typescript-eslint/issues/5274)) ([d806bda](https://github.com/typescript-eslint/typescript-eslint/commit/d806bda82343712a24e3c78b9b34d4345dd1de3b)) +- **scope-manager:** ignore ECMA version ([#5881](https://github.com/typescript-eslint/typescript-eslint/issues/5881)) ([3b8d449](https://github.com/typescript-eslint/typescript-eslint/commit/3b8d449696c319690536a18a48ef32749dc2f559)) +- **typescript-estree:** clarify docs and error for program project without matching TSConfig ([#5762](https://github.com/typescript-eslint/typescript-eslint/issues/5762)) ([67744db](https://github.com/typescript-eslint/typescript-eslint/commit/67744db31f61acab14b5fe027fbc2844ba198c97)) +- **utils:** add `RuleTester` API for top-level dependency constraints ([#5896](https://github.com/typescript-eslint/typescript-eslint/issues/5896)) ([0520d53](https://github.com/typescript-eslint/typescript-eslint/commit/0520d53536af411d66ce2ce0dd478365e67adbac)) +- **website:** Add a happy message to playground output pane when no errors or AST ([#5868](https://github.com/typescript-eslint/typescript-eslint/issues/5868)) ([#5873](https://github.com/typescript-eslint/typescript-eslint/issues/5873)) ([c4e0d86](https://github.com/typescript-eslint/typescript-eslint/commit/c4e0d8678e0398f3ab85510f40ad6f97832b9e6d)) + +### Reverts + +- Revert "feat(scope-manager): ignore ECMA version" (#5888) ([2ee81df](https://github.com/typescript-eslint/typescript-eslint/commit/2ee81df5a365d82ef4b3dfc124d4ec39c7bcb725)), closes [#5888](https://github.com/typescript-eslint/typescript-eslint/issues/5888) [#5881](https://github.com/typescript-eslint/typescript-eslint/issues/5881) + # [5.41.0](https://github.com/typescript-eslint/typescript-eslint/compare/v5.40.1...v5.41.0) (2022-10-24) ### Bug Fixes diff --git a/lerna.json b/lerna.json index 9e451c6323f5..47974603d994 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "5.41.0", + "version": "5.42.0", "npmClient": "yarn", "useWorkspaces": true, "stream": true diff --git a/packages/ast-spec/CHANGELOG.md b/packages/ast-spec/CHANGELOG.md index 5d890f335a6f..4b069d345df9 100644 --- a/packages/ast-spec/CHANGELOG.md +++ b/packages/ast-spec/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [5.42.0](https://github.com/typescript-eslint/typescript-eslint/compare/v5.41.0...v5.42.0) (2022-10-31) + +### Bug Fixes + +- **ast-spec:** add TSQualifiedName to TypeNode union ([#5906](https://github.com/typescript-eslint/typescript-eslint/issues/5906)) ([5c316c1](https://github.com/typescript-eslint/typescript-eslint/commit/5c316c12f09d58aee6ee634a8055533f361f1589)) + # [5.41.0](https://github.com/typescript-eslint/typescript-eslint/compare/v5.40.1...v5.41.0) (2022-10-24) **Note:** Version bump only for package @typescript-eslint/ast-spec diff --git a/packages/ast-spec/package.json b/packages/ast-spec/package.json index 35df9004ba6b..4b35b75dff2b 100644 --- a/packages/ast-spec/package.json +++ b/packages/ast-spec/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/ast-spec", - "version": "5.41.0", + "version": "5.42.0", "description": "TypeScript-ESTree AST spec", "private": true, "keywords": [ diff --git a/packages/eslint-plugin-internal/CHANGELOG.md b/packages/eslint-plugin-internal/CHANGELOG.md index eb09c8881591..150635d9f22b 100644 --- a/packages/eslint-plugin-internal/CHANGELOG.md +++ b/packages/eslint-plugin-internal/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [5.42.0](https://github.com/typescript-eslint/typescript-eslint/compare/v5.41.0...v5.42.0) (2022-10-31) + +**Note:** Version bump only for package @typescript-eslint/eslint-plugin-internal + # [5.41.0](https://github.com/typescript-eslint/typescript-eslint/compare/v5.40.1...v5.41.0) (2022-10-24) **Note:** Version bump only for package @typescript-eslint/eslint-plugin-internal diff --git a/packages/eslint-plugin-internal/package.json b/packages/eslint-plugin-internal/package.json index 489a40472ef4..e21f2c82fa49 100644 --- a/packages/eslint-plugin-internal/package.json +++ b/packages/eslint-plugin-internal/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/eslint-plugin-internal", - "version": "5.41.0", + "version": "5.42.0", "private": true, "main": "dist/index.js", "scripts": { @@ -14,8 +14,8 @@ }, "dependencies": { "@types/prettier": "*", - "@typescript-eslint/scope-manager": "5.41.0", - "@typescript-eslint/utils": "5.41.0", + "@typescript-eslint/scope-manager": "5.42.0", + "@typescript-eslint/utils": "5.42.0", "prettier": "*" } } diff --git a/packages/eslint-plugin-tslint/CHANGELOG.md b/packages/eslint-plugin-tslint/CHANGELOG.md index cf95068ce8eb..758874fd110f 100644 --- a/packages/eslint-plugin-tslint/CHANGELOG.md +++ b/packages/eslint-plugin-tslint/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [5.42.0](https://github.com/typescript-eslint/typescript-eslint/compare/v5.41.0...v5.42.0) (2022-10-31) + +**Note:** Version bump only for package @typescript-eslint/eslint-plugin-tslint + # [5.41.0](https://github.com/typescript-eslint/typescript-eslint/compare/v5.40.1...v5.41.0) (2022-10-24) **Note:** Version bump only for package @typescript-eslint/eslint-plugin-tslint diff --git a/packages/eslint-plugin-tslint/package.json b/packages/eslint-plugin-tslint/package.json index cbe144732873..feeedacebe64 100644 --- a/packages/eslint-plugin-tslint/package.json +++ b/packages/eslint-plugin-tslint/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/eslint-plugin-tslint", - "version": "5.41.0", + "version": "5.42.0", "main": "dist/index.js", "typings": "src/index.ts", "description": "TSLint wrapper plugin for ESLint", @@ -38,7 +38,7 @@ "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { - "@typescript-eslint/utils": "5.41.0", + "@typescript-eslint/utils": "5.42.0", "lodash": "^4.17.21" }, "peerDependencies": { @@ -48,6 +48,6 @@ }, "devDependencies": { "@types/lodash": "*", - "@typescript-eslint/parser": "5.41.0" + "@typescript-eslint/parser": "5.42.0" } } diff --git a/packages/eslint-plugin/CHANGELOG.md b/packages/eslint-plugin/CHANGELOG.md index 7b5a3d1fbc66..609763b480ef 100644 --- a/packages/eslint-plugin/CHANGELOG.md +++ b/packages/eslint-plugin/CHANGELOG.md @@ -3,6 +3,18 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [5.42.0](https://github.com/typescript-eslint/typescript-eslint/compare/v5.41.0...v5.42.0) (2022-10-31) + +### Bug Fixes + +- **ast-spec:** add TSQualifiedName to TypeNode union ([#5906](https://github.com/typescript-eslint/typescript-eslint/issues/5906)) ([5c316c1](https://github.com/typescript-eslint/typescript-eslint/commit/5c316c12f09d58aee6ee634a8055533f361f1589)) +- **eslint-plugin:** [no-extra-parens] handle type assertion in extends clause ([#5901](https://github.com/typescript-eslint/typescript-eslint/issues/5901)) ([8ed7219](https://github.com/typescript-eslint/typescript-eslint/commit/8ed72192c274249d26628fb125796e71318b857a)) + +### Features + +- **eslint-plugin:** [member-ordering] add natural sort order ([#5662](https://github.com/typescript-eslint/typescript-eslint/issues/5662)) ([1eaae09](https://github.com/typescript-eslint/typescript-eslint/commit/1eaae09ecca359f366b94f6a04665403f48b05c7)) +- **eslint-plugin:** [no-invalid-void-type] better report message for void used as a constituent inside a function return type ([#5274](https://github.com/typescript-eslint/typescript-eslint/issues/5274)) ([d806bda](https://github.com/typescript-eslint/typescript-eslint/commit/d806bda82343712a24e3c78b9b34d4345dd1de3b)) + # [5.41.0](https://github.com/typescript-eslint/typescript-eslint/compare/v5.40.1...v5.41.0) (2022-10-24) ### Bug Fixes diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index eb1c777d1154..f3afe646a16c 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/eslint-plugin", - "version": "5.41.0", + "version": "5.42.0", "description": "TypeScript plugin for ESLint", "keywords": [ "eslint", @@ -44,9 +44,9 @@ "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { - "@typescript-eslint/scope-manager": "5.41.0", - "@typescript-eslint/type-utils": "5.41.0", - "@typescript-eslint/utils": "5.41.0", + "@typescript-eslint/scope-manager": "5.42.0", + "@typescript-eslint/type-utils": "5.42.0", + "@typescript-eslint/utils": "5.42.0", "debug": "^4.3.4", "ignore": "^5.2.0", "natural-compare-lite": "^1.4.0", diff --git a/packages/experimental-utils/CHANGELOG.md b/packages/experimental-utils/CHANGELOG.md index 5b2bc9b5d9bd..7334d6b80b4d 100644 --- a/packages/experimental-utils/CHANGELOG.md +++ b/packages/experimental-utils/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [5.42.0](https://github.com/typescript-eslint/typescript-eslint/compare/v5.41.0...v5.42.0) (2022-10-31) + +**Note:** Version bump only for package @typescript-eslint/experimental-utils + # [5.41.0](https://github.com/typescript-eslint/typescript-eslint/compare/v5.40.1...v5.41.0) (2022-10-24) **Note:** Version bump only for package @typescript-eslint/experimental-utils diff --git a/packages/experimental-utils/package.json b/packages/experimental-utils/package.json index 8056c69d6586..618afe32e044 100644 --- a/packages/experimental-utils/package.json +++ b/packages/experimental-utils/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/experimental-utils", - "version": "5.41.0", + "version": "5.42.0", "description": "(Experimental) Utilities for working with TypeScript + ESLint together", "keywords": [ "eslint", @@ -38,7 +38,7 @@ "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { - "@typescript-eslint/utils": "5.41.0" + "@typescript-eslint/utils": "5.42.0" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" diff --git a/packages/parser/CHANGELOG.md b/packages/parser/CHANGELOG.md index b8064f16b73e..689b11e82276 100644 --- a/packages/parser/CHANGELOG.md +++ b/packages/parser/CHANGELOG.md @@ -3,6 +3,16 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [5.42.0](https://github.com/typescript-eslint/typescript-eslint/compare/v5.41.0...v5.42.0) (2022-10-31) + +### Features + +- **scope-manager:** ignore ECMA version ([#5881](https://github.com/typescript-eslint/typescript-eslint/issues/5881)) ([3b8d449](https://github.com/typescript-eslint/typescript-eslint/commit/3b8d449696c319690536a18a48ef32749dc2f559)) + +### Reverts + +- Revert "feat(scope-manager): ignore ECMA version" (#5888) ([2ee81df](https://github.com/typescript-eslint/typescript-eslint/commit/2ee81df5a365d82ef4b3dfc124d4ec39c7bcb725)), closes [#5888](https://github.com/typescript-eslint/typescript-eslint/issues/5888) [#5881](https://github.com/typescript-eslint/typescript-eslint/issues/5881) + # [5.41.0](https://github.com/typescript-eslint/typescript-eslint/compare/v5.40.1...v5.41.0) (2022-10-24) **Note:** Version bump only for package @typescript-eslint/parser diff --git a/packages/parser/package.json b/packages/parser/package.json index 66e27e0d0a77..8ef9a0cf3592 100644 --- a/packages/parser/package.json +++ b/packages/parser/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/parser", - "version": "5.41.0", + "version": "5.42.0", "description": "An ESLint custom parser which leverages TypeScript ESTree", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -45,9 +45,9 @@ "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "dependencies": { - "@typescript-eslint/scope-manager": "5.41.0", - "@typescript-eslint/types": "5.41.0", - "@typescript-eslint/typescript-estree": "5.41.0", + "@typescript-eslint/scope-manager": "5.42.0", + "@typescript-eslint/types": "5.42.0", + "@typescript-eslint/typescript-estree": "5.42.0", "debug": "^4.3.4" }, "devDependencies": { diff --git a/packages/scope-manager/CHANGELOG.md b/packages/scope-manager/CHANGELOG.md index 66b6e6b4f53c..a4601cd80b35 100644 --- a/packages/scope-manager/CHANGELOG.md +++ b/packages/scope-manager/CHANGELOG.md @@ -3,6 +3,16 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [5.42.0](https://github.com/typescript-eslint/typescript-eslint/compare/v5.41.0...v5.42.0) (2022-10-31) + +### Features + +- **scope-manager:** ignore ECMA version ([#5881](https://github.com/typescript-eslint/typescript-eslint/issues/5881)) ([3b8d449](https://github.com/typescript-eslint/typescript-eslint/commit/3b8d449696c319690536a18a48ef32749dc2f559)) + +### Reverts + +- Revert "feat(scope-manager): ignore ECMA version" (#5888) ([2ee81df](https://github.com/typescript-eslint/typescript-eslint/commit/2ee81df5a365d82ef4b3dfc124d4ec39c7bcb725)), closes [#5888](https://github.com/typescript-eslint/typescript-eslint/issues/5888) [#5881](https://github.com/typescript-eslint/typescript-eslint/issues/5881) + # [5.41.0](https://github.com/typescript-eslint/typescript-eslint/compare/v5.40.1...v5.41.0) (2022-10-24) **Note:** Version bump only for package @typescript-eslint/scope-manager diff --git a/packages/scope-manager/package.json b/packages/scope-manager/package.json index 5bc61a61f3b9..34ffe2f86e93 100644 --- a/packages/scope-manager/package.json +++ b/packages/scope-manager/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/scope-manager", - "version": "5.41.0", + "version": "5.42.0", "description": "TypeScript scope analyser for ESLint", "keywords": [ "eslint", @@ -38,12 +38,12 @@ "typecheck": "cd ../../ && nx typecheck @typescript-eslint/scope-manager" }, "dependencies": { - "@typescript-eslint/types": "5.41.0", - "@typescript-eslint/visitor-keys": "5.41.0" + "@typescript-eslint/types": "5.42.0", + "@typescript-eslint/visitor-keys": "5.42.0" }, "devDependencies": { "@types/glob": "*", - "@typescript-eslint/typescript-estree": "5.41.0", + "@typescript-eslint/typescript-estree": "5.42.0", "glob": "*", "jest-specific-snapshot": "*", "make-dir": "*", diff --git a/packages/shared-fixtures/CHANGELOG.md b/packages/shared-fixtures/CHANGELOG.md index ba001fbfa5da..6011551b73ef 100644 --- a/packages/shared-fixtures/CHANGELOG.md +++ b/packages/shared-fixtures/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [5.42.0](https://github.com/typescript-eslint/typescript-eslint/compare/v5.41.0...v5.42.0) (2022-10-31) + +**Note:** Version bump only for package @typescript-eslint/shared-fixtures + # [5.41.0](https://github.com/typescript-eslint/typescript-eslint/compare/v5.40.1...v5.41.0) (2022-10-24) **Note:** Version bump only for package @typescript-eslint/shared-fixtures diff --git a/packages/shared-fixtures/package.json b/packages/shared-fixtures/package.json index 864ddbf70bb3..5c8bb352ba39 100644 --- a/packages/shared-fixtures/package.json +++ b/packages/shared-fixtures/package.json @@ -1,5 +1,5 @@ { "name": "@typescript-eslint/shared-fixtures", - "version": "5.41.0", + "version": "5.42.0", "private": true } diff --git a/packages/type-utils/CHANGELOG.md b/packages/type-utils/CHANGELOG.md index 09b3607c2019..09af50ebb31e 100644 --- a/packages/type-utils/CHANGELOG.md +++ b/packages/type-utils/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [5.42.0](https://github.com/typescript-eslint/typescript-eslint/compare/v5.41.0...v5.42.0) (2022-10-31) + +**Note:** Version bump only for package @typescript-eslint/type-utils + # [5.41.0](https://github.com/typescript-eslint/typescript-eslint/compare/v5.40.1...v5.41.0) (2022-10-24) ### Bug Fixes diff --git a/packages/type-utils/package.json b/packages/type-utils/package.json index bdc68f60358a..6150b2d27c9b 100644 --- a/packages/type-utils/package.json +++ b/packages/type-utils/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/type-utils", - "version": "5.41.0", + "version": "5.42.0", "description": "Type utilities for working with TypeScript + ESLint together", "keywords": [ "eslint", @@ -39,13 +39,13 @@ "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { - "@typescript-eslint/typescript-estree": "5.41.0", - "@typescript-eslint/utils": "5.41.0", + "@typescript-eslint/typescript-estree": "5.42.0", + "@typescript-eslint/utils": "5.42.0", "debug": "^4.3.4", "tsutils": "^3.21.0" }, "devDependencies": { - "@typescript-eslint/parser": "5.41.0", + "@typescript-eslint/parser": "5.42.0", "typescript": "*" }, "peerDependencies": { diff --git a/packages/types/CHANGELOG.md b/packages/types/CHANGELOG.md index e7cfbe3fb88a..48a98e9a16fe 100644 --- a/packages/types/CHANGELOG.md +++ b/packages/types/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [5.42.0](https://github.com/typescript-eslint/typescript-eslint/compare/v5.41.0...v5.42.0) (2022-10-31) + +**Note:** Version bump only for package @typescript-eslint/types + # [5.41.0](https://github.com/typescript-eslint/typescript-eslint/compare/v5.40.1...v5.41.0) (2022-10-24) **Note:** Version bump only for package @typescript-eslint/types diff --git a/packages/types/package.json b/packages/types/package.json index 78c880438fba..d2347b6a48cd 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/types", - "version": "5.41.0", + "version": "5.42.0", "description": "Types for the TypeScript-ESTree AST spec", "keywords": [ "eslint", diff --git a/packages/typescript-estree/CHANGELOG.md b/packages/typescript-estree/CHANGELOG.md index cb369c72bf4f..78cf8e7aefc2 100644 --- a/packages/typescript-estree/CHANGELOG.md +++ b/packages/typescript-estree/CHANGELOG.md @@ -3,6 +3,16 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [5.42.0](https://github.com/typescript-eslint/typescript-eslint/compare/v5.41.0...v5.42.0) (2022-10-31) + +### Bug Fixes + +- **typescript-estree:** don't allow single-run unless we're in type-aware linting mode ([#5893](https://github.com/typescript-eslint/typescript-eslint/issues/5893)) ([891b087](https://github.com/typescript-eslint/typescript-eslint/commit/891b0879ba9c64a4722b8c0bf9e599a725b6d6df)) + +### Features + +- **typescript-estree:** clarify docs and error for program project without matching TSConfig ([#5762](https://github.com/typescript-eslint/typescript-eslint/issues/5762)) ([67744db](https://github.com/typescript-eslint/typescript-eslint/commit/67744db31f61acab14b5fe027fbc2844ba198c97)) + # [5.41.0](https://github.com/typescript-eslint/typescript-eslint/compare/v5.40.1...v5.41.0) (2022-10-24) **Note:** Version bump only for package @typescript-eslint/typescript-estree diff --git a/packages/typescript-estree/package.json b/packages/typescript-estree/package.json index 52de436db7bd..1df5908ca03c 100644 --- a/packages/typescript-estree/package.json +++ b/packages/typescript-estree/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/typescript-estree", - "version": "5.41.0", + "version": "5.42.0", "description": "A parser that converts TypeScript source code into an ESTree compatible form", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -42,8 +42,8 @@ "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { - "@typescript-eslint/types": "5.41.0", - "@typescript-eslint/visitor-keys": "5.41.0", + "@typescript-eslint/types": "5.42.0", + "@typescript-eslint/visitor-keys": "5.42.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -59,7 +59,7 @@ "@types/is-glob": "*", "@types/semver": "*", "@types/tmp": "*", - "@typescript-eslint/shared-fixtures": "5.41.0", + "@typescript-eslint/shared-fixtures": "5.42.0", "glob": "*", "jest-specific-snapshot": "*", "make-dir": "*", diff --git a/packages/utils/CHANGELOG.md b/packages/utils/CHANGELOG.md index ea1b24da025e..97a2797ea8ad 100644 --- a/packages/utils/CHANGELOG.md +++ b/packages/utils/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [5.42.0](https://github.com/typescript-eslint/typescript-eslint/compare/v5.41.0...v5.42.0) (2022-10-31) + +### Features + +- **utils:** add `RuleTester` API for top-level dependency constraints ([#5896](https://github.com/typescript-eslint/typescript-eslint/issues/5896)) ([0520d53](https://github.com/typescript-eslint/typescript-eslint/commit/0520d53536af411d66ce2ce0dd478365e67adbac)) + # [5.41.0](https://github.com/typescript-eslint/typescript-eslint/compare/v5.40.1...v5.41.0) (2022-10-24) **Note:** Version bump only for package @typescript-eslint/utils diff --git a/packages/utils/package.json b/packages/utils/package.json index 21b80b4a9fa1..2de743ab01dc 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/utils", - "version": "5.41.0", + "version": "5.42.0", "description": "Utilities for working with TypeScript + ESLint together", "keywords": [ "eslint", @@ -41,9 +41,9 @@ "dependencies": { "@types/json-schema": "^7.0.9", "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.41.0", - "@typescript-eslint/types": "5.41.0", - "@typescript-eslint/typescript-estree": "5.41.0", + "@typescript-eslint/scope-manager": "5.42.0", + "@typescript-eslint/types": "5.42.0", + "@typescript-eslint/typescript-estree": "5.42.0", "eslint-scope": "^5.1.1", "eslint-utils": "^3.0.0", "semver": "^7.3.7" @@ -52,7 +52,7 @@ "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "devDependencies": { - "@typescript-eslint/parser": "5.41.0", + "@typescript-eslint/parser": "5.42.0", "typescript": "*" }, "funding": { diff --git a/packages/visitor-keys/CHANGELOG.md b/packages/visitor-keys/CHANGELOG.md index 65df1dacb426..21700ddd1a37 100644 --- a/packages/visitor-keys/CHANGELOG.md +++ b/packages/visitor-keys/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [5.42.0](https://github.com/typescript-eslint/typescript-eslint/compare/v5.41.0...v5.42.0) (2022-10-31) + +**Note:** Version bump only for package @typescript-eslint/visitor-keys + # [5.41.0](https://github.com/typescript-eslint/typescript-eslint/compare/v5.40.1...v5.41.0) (2022-10-24) **Note:** Version bump only for package @typescript-eslint/visitor-keys diff --git a/packages/visitor-keys/package.json b/packages/visitor-keys/package.json index 9898d07c851c..2068a094820e 100644 --- a/packages/visitor-keys/package.json +++ b/packages/visitor-keys/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/visitor-keys", - "version": "5.41.0", + "version": "5.42.0", "description": "Visitor keys used to help traverse the TypeScript-ESTree AST", "keywords": [ "eslint", @@ -39,7 +39,7 @@ "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { - "@typescript-eslint/types": "5.41.0", + "@typescript-eslint/types": "5.42.0", "eslint-visitor-keys": "^3.3.0" }, "devDependencies": { diff --git a/packages/website-eslint/CHANGELOG.md b/packages/website-eslint/CHANGELOG.md index 2129728bf184..5c65303053e2 100644 --- a/packages/website-eslint/CHANGELOG.md +++ b/packages/website-eslint/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [5.42.0](https://github.com/typescript-eslint/typescript-eslint/compare/v5.41.0...v5.42.0) (2022-10-31) + +**Note:** Version bump only for package @typescript-eslint/website-eslint + # [5.41.0](https://github.com/typescript-eslint/typescript-eslint/compare/v5.40.1...v5.41.0) (2022-10-24) **Note:** Version bump only for package @typescript-eslint/website-eslint diff --git a/packages/website-eslint/package.json b/packages/website-eslint/package.json index f094f70c3be4..5384c756758c 100644 --- a/packages/website-eslint/package.json +++ b/packages/website-eslint/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/website-eslint", - "version": "5.41.0", + "version": "5.42.0", "private": true, "description": "ESLint which works in browsers.", "engines": { @@ -16,19 +16,19 @@ "format": "prettier --write \"./**/*.{ts,mts,cts,tsx,js,mjs,cjs,jsx,json,md,css}\" --ignore-path ../../.prettierignore" }, "dependencies": { - "@typescript-eslint/types": "5.41.0", - "@typescript-eslint/utils": "5.41.0" + "@typescript-eslint/types": "5.42.0", + "@typescript-eslint/utils": "5.42.0" }, "devDependencies": { "@rollup/plugin-commonjs": "^23.0.0", "@rollup/plugin-json": "^5.0.0", "@rollup/plugin-node-resolve": "^15.0.0", "@rollup/pluginutils": "^5.0.0", - "@typescript-eslint/eslint-plugin": "5.41.0", - "@typescript-eslint/parser": "5.41.0", - "@typescript-eslint/scope-manager": "5.41.0", - "@typescript-eslint/typescript-estree": "5.41.0", - "@typescript-eslint/visitor-keys": "5.41.0", + "@typescript-eslint/eslint-plugin": "5.42.0", + "@typescript-eslint/parser": "5.42.0", + "@typescript-eslint/scope-manager": "5.42.0", + "@typescript-eslint/typescript-estree": "5.42.0", + "@typescript-eslint/visitor-keys": "5.42.0", "eslint": "*", "rollup": "^2.75.4", "rollup-plugin-terser": "^7.0.2", diff --git a/packages/website/CHANGELOG.md b/packages/website/CHANGELOG.md index 38d0670bfbc9..472da3ec4f2e 100644 --- a/packages/website/CHANGELOG.md +++ b/packages/website/CHANGELOG.md @@ -3,6 +3,21 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [5.42.0](https://github.com/typescript-eslint/typescript-eslint/compare/v5.41.0...v5.42.0) (2022-10-31) + +### Bug Fixes + +- **eslint-plugin:** enable react/jsx-curly-brace-presence lint rule in website package ([#5894](https://github.com/typescript-eslint/typescript-eslint/issues/5894)) ([344322a](https://github.com/typescript-eslint/typescript-eslint/commit/344322add846d03c6c9981e486b09e6ba1196555)) + +### Features + +- **scope-manager:** ignore ECMA version ([#5881](https://github.com/typescript-eslint/typescript-eslint/issues/5881)) ([3b8d449](https://github.com/typescript-eslint/typescript-eslint/commit/3b8d449696c319690536a18a48ef32749dc2f559)) +- **website:** Add a happy message to playground output pane when no errors or AST ([#5868](https://github.com/typescript-eslint/typescript-eslint/issues/5868)) ([#5873](https://github.com/typescript-eslint/typescript-eslint/issues/5873)) ([c4e0d86](https://github.com/typescript-eslint/typescript-eslint/commit/c4e0d8678e0398f3ab85510f40ad6f97832b9e6d)) + +### Reverts + +- Revert "feat(scope-manager): ignore ECMA version" (#5888) ([2ee81df](https://github.com/typescript-eslint/typescript-eslint/commit/2ee81df5a365d82ef4b3dfc124d4ec39c7bcb725)), closes [#5888](https://github.com/typescript-eslint/typescript-eslint/issues/5888) [#5881](https://github.com/typescript-eslint/typescript-eslint/issues/5881) + # [5.41.0](https://github.com/typescript-eslint/typescript-eslint/compare/v5.40.1...v5.41.0) (2022-10-24) **Note:** Version bump only for package website diff --git a/packages/website/package.json b/packages/website/package.json index 9469e3450a7f..c2ef8addd0e6 100644 --- a/packages/website/package.json +++ b/packages/website/package.json @@ -1,6 +1,6 @@ { "name": "website", - "version": "5.41.0", + "version": "5.42.0", "private": true, "scripts": { "build": "docusaurus build", @@ -21,8 +21,8 @@ "@docusaurus/remark-plugin-npm2yarn": "~2.1.0", "@docusaurus/theme-common": "~2.1.0", "@mdx-js/react": "1.6.22", - "@typescript-eslint/parser": "5.41.0", - "@typescript-eslint/website-eslint": "5.41.0", + "@typescript-eslint/parser": "5.42.0", + "@typescript-eslint/website-eslint": "5.42.0", "clsx": "^1.1.1", "eslint": "*", "json-schema": "^0.4.0", @@ -48,7 +48,7 @@ "@types/react": "^18.0.9", "@types/react-helmet": "^6.1.5", "@types/react-router-dom": "^5.3.3", - "@typescript-eslint/eslint-plugin": "5.41.0", + "@typescript-eslint/eslint-plugin": "5.42.0", "copy-webpack-plugin": "^11.0.0", "eslint-plugin-jsx-a11y": "^6.5.1", "eslint-plugin-react": "^7.29.4",