diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index cf0073d35f02..67428be1e01e 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -73,6 +73,7 @@ i.e. eslint --ext ".ts,.js" src --debug | `@typescript-eslint/parser` | `X.Y.Z` | | `@typescript-eslint/typescript-estree` | `X.Y.Z` | | `@typescript-eslint/experimental-utils` | `X.Y.Z` | +| `@typescript-eslint/type-utils` | `X.Y.Z` | | `TypeScript` | `X.Y.Z` | | `node` | `X.Y.Z` | | `npm` | `X.Y.Z` | diff --git a/.github/ISSUE_TEMPLATE/typescript-eslint-type-utils.md b/.github/ISSUE_TEMPLATE/typescript-eslint-type-utils.md new file mode 100644 index 000000000000..c2f8229829ce --- /dev/null +++ b/.github/ISSUE_TEMPLATE/typescript-eslint-type-utils.md @@ -0,0 +1,72 @@ +--- +name: '@typescript-eslint/type-utils' +about: Report an issue with the '@typescript-eslint/type-utils' package +title: '' +labels: 'package: type-utils, triage' +assignees: '' +--- + + + + + +- [ ] I have tried restarting my IDE and the issue persists. +- [ ] I have updated to the latest version of the packages. +- [ ] I have [read the FAQ](https://github.com/typescript-eslint/typescript-eslint/blob/master/docs/getting-started/linting/FAQ.md) and my problem is not listed. + +**Repro** + + + +```TS +// your repro code case +``` + +**Expected Result** + + + +**Actual Result** + + + +**Additional Info** + + + +**Versions** + +| package | version | +| ------------------------------- | ------- | +| `@typescript-eslint/type-utils` | `X.Y.Z` | +| `@typescript-eslint/type-utils` | `X.Y.Z` | +| `TypeScript` | `X.Y.Z` | +| `node` | `X.Y.Z` | diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7af2bdb414ee..f05912ef76a5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -116,6 +116,11 @@ jobs: env: CI: true + - name: Run unit tests for type-utils + run: npx nx test @typescript-eslint/type-utils + env: + CI: true + - name: Run unit tests for parser run: npx nx test @typescript-eslint/parser env: @@ -283,6 +288,11 @@ jobs: env: CI: true + - name: Run unit tests for type-utils + run: npx nx test @typescript-eslint/type-utils + env: + CI: true + - name: Run unit tests for parser run: npx nx test @typescript-eslint/parser env: diff --git a/.vscode/launch.json b/.vscode/launch.json index 5ad1e2264578..0162b9ae8312 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -24,6 +24,8 @@ "${workspaceFolder}/packages/experimental-utils/dist/index.js", "${workspaceFolder}/packages/experimental-utils/src/ts-estree.ts", "${workspaceFolder}/packages/experimental-utils/dist/ts-estree.js", + "${workspaceFolder}/packages/type-utils/src/index.ts", + "${workspaceFolder}/packages/type-utils/dist/index.js", "${workspaceFolder}/packages/parser/src/index.ts", "${workspaceFolder}/packages/parser/dist/index.js", "${workspaceFolder}/packages/typescript-estree/src/index.ts", @@ -56,6 +58,8 @@ "${workspaceFolder}/packages/experimental-utils/dist/index.js", "${workspaceFolder}/packages/experimental-utils/src/ts-estree.ts", "${workspaceFolder}/packages/experimental-utils/dist/ts-estree.js", + "${workspaceFolder}/packages/type-utils/src/index.ts", + "${workspaceFolder}/packages/type-utils/dist/index.js", "${workspaceFolder}/packages/parser/src/index.ts", "${workspaceFolder}/packages/parser/dist/index.js", "${workspaceFolder}/packages/typescript-estree/src/index.ts", @@ -88,6 +92,8 @@ "${workspaceFolder}/packages/experimental-utils/dist/index.js", "${workspaceFolder}/packages/experimental-utils/src/ts-estree.ts", "${workspaceFolder}/packages/experimental-utils/dist/ts-estree.js", + "${workspaceFolder}/packages/type-utils/src/ts-estree.ts", + "${workspaceFolder}/packages/type-utils/dist/ts-estree.js", "${workspaceFolder}/packages/parser/src/index.ts", "${workspaceFolder}/packages/parser/dist/index.js", "${workspaceFolder}/packages/typescript-estree/src/index.ts", @@ -120,6 +126,8 @@ "${workspaceFolder}/packages/experimental-utils/dist/index.js", "${workspaceFolder}/packages/experimental-utils/src/ts-estree.ts", "${workspaceFolder}/packages/experimental-utils/dist/ts-estree.js", + "${workspaceFolder}/packages/type-utils/src/index.ts", + "${workspaceFolder}/packages/type-utils/dist/index.js", "${workspaceFolder}/packages/parser/src/index.ts", "${workspaceFolder}/packages/parser/dist/index.js", "${workspaceFolder}/packages/typescript-estree/src/index.ts", @@ -152,6 +160,42 @@ "${workspaceFolder}/packages/experimental-utils/dist/index.js", "${workspaceFolder}/packages/experimental-utils/src/ts-estree.ts", "${workspaceFolder}/packages/experimental-utils/dist/ts-estree.js", + "${workspaceFolder}/packages/type-utils/src/index.ts", + "${workspaceFolder}/packages/type-utils/dist/index.js", + "${workspaceFolder}/packages/parser/src/index.ts", + "${workspaceFolder}/packages/parser/dist/index.js", + "${workspaceFolder}/packages/typescript-estree/src/index.ts", + "${workspaceFolder}/packages/typescript-estree/dist/index.js", + "${workspaceFolder}/packages/types/src/index.ts", + "${workspaceFolder}/packages/types/dist/index.js", + "${workspaceFolder}/packages/visitor-keys/src/index.ts", + "${workspaceFolder}/packages/visitor-keys/dist/index.js", + "${workspaceFolder}/packages/scope-manager/dist/index.js", + "${workspaceFolder}/packages/scope-manager/dist/index.js", + ], + }, + { + "type": "node", + "request": "launch", + "name": "Run currently opened type-utils test", + "cwd": "${workspaceFolder}/packages/type-utils/", + "program": "${workspaceFolder}/node_modules/jest/bin/jest.js", + "args": [ + "--runInBand", + "--no-cache", + "--no-coverage", + "${fileBasenameNoExtension}" + ], + "sourceMaps": true, + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "skipFiles": [ + "${workspaceFolder}/packages/experimental-utils/src/index.ts", + "${workspaceFolder}/packages/experimental-utils/dist/index.js", + "${workspaceFolder}/packages/experimental-utils/src/ts-estree.ts", + "${workspaceFolder}/packages/experimental-utils/dist/ts-estree.js", + "${workspaceFolder}/packages/type-utils/src/index.ts", + "${workspaceFolder}/packages/type-utils/dist/index.js", "${workspaceFolder}/packages/parser/src/index.ts", "${workspaceFolder}/packages/parser/dist/index.js", "${workspaceFolder}/packages/typescript-estree/src/index.ts", @@ -184,6 +228,8 @@ "${workspaceFolder}/packages/experimental-utils/dist/index.js", "${workspaceFolder}/packages/experimental-utils/src/ts-estree.ts", "${workspaceFolder}/packages/experimental-utils/dist/ts-estree.js", + "${workspaceFolder}/packages/type-utils/src/index.ts", + "${workspaceFolder}/packages/type-utils/dist/index.js", "${workspaceFolder}/packages/parser/src/index.ts", "${workspaceFolder}/packages/parser/dist/index.js", "${workspaceFolder}/packages/typescript-estree/src/index.ts", diff --git a/CHANGELOG.md b/CHANGELOG.md index 055234ca9a85..b4d21d2a2496 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [5.9.0](https://github.com/typescript-eslint/typescript-eslint/compare/v5.8.1...v5.9.0) (2022-01-03) + + +### Features + +* **experimental-utils:** move isTypeReadonly from eslint-plugin to experimental-utils ([#3658](https://github.com/typescript-eslint/typescript-eslint/issues/3658)) ([a9eb0b9](https://github.com/typescript-eslint/typescript-eslint/commit/a9eb0b9eb2db291ea36065ec34f84bf5c5504b43)) + + + + + ## [5.8.1](https://github.com/typescript-eslint/typescript-eslint/compare/v5.8.0...v5.8.1) (2021-12-27) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 42ef14b19c5e..c147ef537fcb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -53,6 +53,7 @@ We have a sophisticated CI process setup which gets run on every PR. You must pa - You can run `yarn typecheck` in any package or in the root. - Ensure your changes are adequately tested. - You can run `yarn test` in any package. + - [VS Code launch tasks](https://code.visualstudio.com/docs/editor/tasks) tasks are provided that allow [visual debugging](https://code.visualstudio.com/docs/editor/debugging) tests - We aim for around `90%` branch coverage for every PR. - Coverage reports should automatically be generated locally, and the `codecov` bot should also comment on your PR with the percentage, as well as links to the line-by-line coverage of each file touched by your PR. - Ensure you have no lint errors. diff --git a/docs/development/architecture/ASTS.md b/docs/development/architecture/ASTS.md index 09526ac611a5..26164c8174e1 100644 --- a/docs/development/architecture/ASTS.md +++ b/docs/development/architecture/ASTS.md @@ -41,9 +41,14 @@ ESLint uses an AST format known as **[`estree`]**. ESTree is more broadly used than just for ESLint -- it is the de facto community standard. ESLint's built-in parser that outputs an `estree`-shaped AST is also a separate package, called **[`espree`]**. +## AST Playground + +The [TypeScript ESLint playground](https://typescript-eslint.io/play#showAST=es) contains an AST explorer that generates an interactive AST for any code entered into the playground. +You can activate it under _Options_ > _AST Explorer_ on its left sidebar by selecting _ESTree_. + :::note -You can play more with various ASTs such as ESTree on [astexplorer.net] and read more details on their [Wikipedia article](https://en.wikipedia.org/wiki/Abstract_syntax_tree). +You can play more with various other ASTs on [astexplorer.net] and read more details on their [Wikipedia article](https://en.wikipedia.org/wiki/Abstract_syntax_tree). ::: diff --git a/docs/development/architecture/PACKAGES.md b/docs/development/architecture/PACKAGES.md index a212629ce77e..1dcff05a2574 100644 --- a/docs/development/architecture/PACKAGES.md +++ b/docs/development/architecture/PACKAGES.md @@ -32,7 +32,7 @@ ESTree is unoptimized and intended for "general purpose" use-cases of traversing See more on configuring custom parsers with ESLint on [ESLint's User Guide > Configuring > Plugins](https://eslint.org/docs/user-guide/configuring/plugins#specifying-parser). :::tip -You can select `@typescript-eslint/parser` on [astexplorer.net](https://astexplorer.net)'s top-middle βš™ dropdown that defaults to Acorn. +You can select `@typescript-eslint/parser` on the [TypeScript ESLint playground](https://typescript-eslint.io/play#showAST=es)'s left sidebar under _Options_ > _AST Explorer_ by selecting _ESTree_. ::: ## `@typescript-eslint/typescript-estree` diff --git a/docs/linting/TROUBLESHOOTING.md b/docs/linting/TROUBLESHOOTING.md index 80871c2f9c5f..8f8ae10565de 100644 --- a/docs/linting/TROUBLESHOOTING.md +++ b/docs/linting/TROUBLESHOOTING.md @@ -67,7 +67,7 @@ See [#2041](https://github.com/typescript-eslint/typescript-eslint/issues/2041) ESLint core contains the rule [`no-restricted-syntax`](https://eslint.org/docs/rules/no-restricted-syntax). This generic rule allows you to specify a [selector](https://eslint.org/docs/developer-guide/selectors) for the code you want to ban, along with a custom error message. -You can use a tool like [AST Explorer](https://astexplorer.net/) to help in figuring out the structure of the AST that you want to ban. +You can use an AST visualization tool such as [TypeScript ESLint playground](https://typescript-eslint.io/play#showAST=es) > _Options_ > _AST Explorer_ on its left sidebar by selecting _ESTree_ to help in figuring out the structure of the AST that you want to ban. For example, you can ban enums (or some variation of) using one of the following configs: @@ -153,6 +153,19 @@ If you see more than one version installed, then you will have to either use [ya **The best course of action in this case is to wait until your dependency releases a new version with support for our latest versions.** +## How can I specify a TypeScript version / `parserOptions.typescriptLocation`? + +You can't, and you don't want to. + +You should use the same version of TypeScript for linting as the rest of your project. +TypeScript versions often have slight differences in edge cases that can cause contradictory information between typescript-eslint rules and editor information. +For example: + +- `@typescript-eslint/strict-boolean-expressions` might be operating with TypeScript version _X_ and think a variable is `string[] | undefined` +- TypeScript itself might be on version _X+1-beta_ and think the variable is `string[]` + +See [this issue comment](https://github.com/typescript-eslint/typescript-eslint/issues/4102#issuecomment-963265514) for more details. + ## My linting feels really slow As mentioned in the [type-aware linting doc](./TYPED_LINTING.md), if you're using type-aware linting, your lint times should be roughly the same as your build times. @@ -170,6 +183,18 @@ Additionally, if you provide no `include` in your tsconfig, then it is the same Wide globs can cause TypeScript to parse things like build artifacts, which can heavily impact performance. Always ensure you provide globs targeted at the folders you are specifically wanting to lint. +### Wide includes in your ESLint options + +Specifying `tsconfig.json` paths in your ESLint commands is also likely to cause much more disk IO than expected. +Instead of globs that use `**` to recursively check all folders, prefer paths that use a single `*` at a time. + +```diff +- eslint --parser-options project:./**/tsconfig.json ++ eslint --parser-options project:./packages/*/tsconfig.json +``` + +See [Glob pattern in parser's option "project" slows down linting](https://github.com/typescript-eslint/typescript-eslint/issues/2611) for more details. + ### `eslint-plugin-prettier` This plugin surfaces prettier formatting problems at lint time, helping to ensure your code is always formatted. diff --git a/docs/linting/TYPED_LINTING.md b/docs/linting/TYPED_LINTING.md index ca86391aed14..376a0334c174 100644 --- a/docs/linting/TYPED_LINTING.md +++ b/docs/linting/TYPED_LINTING.md @@ -60,6 +60,8 @@ Depending on what you want to achieve: - 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: diff --git a/lerna.json b/lerna.json index 1db17c7a68fb..a0000d734dd9 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "5.8.1", + "version": "5.9.0", "npmClient": "yarn", "useWorkspaces": true, "stream": true diff --git a/package.json b/package.json index 7c532099e9bb..d2df363bc502 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,6 @@ "start": "nx run website:start", "test": "nx run-many --target=test --all --parallel", "test-integration": "yarn jest -c ./tests/integration/jest.config.js", - "test-kill-integration-containers": "docker-compose -f tests/integration/docker-compose.yml down -v --rmi local", "typecheck": "nx run-many --target=typecheck --all --parallel" }, "config": { @@ -60,7 +59,7 @@ "@babel/parser": "^7.16.4", "@babel/types": "^7.16.0", "@commitlint/cli": "^15.0.0", - "@commitlint/config-conventional": "^15.0.0", + "@commitlint/config-conventional": "^16.0.0", "@nrwl/cli": "13.0.2", "@nrwl/nx-cloud": "12.5.1", "@nrwl/tao": "13.0.2", diff --git a/packages/ast-spec/CHANGELOG.md b/packages/ast-spec/CHANGELOG.md index 2da2ffc8abf9..a1b2bcd55efa 100644 --- a/packages/ast-spec/CHANGELOG.md +++ b/packages/ast-spec/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [5.9.0](https://github.com/typescript-eslint/typescript-eslint/compare/v5.8.1...v5.9.0) (2022-01-03) + + +### Features + +* **experimental-utils:** move isTypeReadonly from eslint-plugin to experimental-utils ([#3658](https://github.com/typescript-eslint/typescript-eslint/issues/3658)) ([a9eb0b9](https://github.com/typescript-eslint/typescript-eslint/commit/a9eb0b9eb2db291ea36065ec34f84bf5c5504b43)) + + + + + ## [5.8.1](https://github.com/typescript-eslint/typescript-eslint/compare/v5.8.0...v5.8.1) (2021-12-27) **Note:** Version bump only for package @typescript-eslint/ast-spec diff --git a/packages/ast-spec/README.md b/packages/ast-spec/README.md index 7c954398395b..6d29fecd24ec 100644 --- a/packages/ast-spec/README.md +++ b/packages/ast-spec/README.md @@ -16,7 +16,7 @@ It includes: **You probably don't want to use it directly.** -If you're building an ESLint plugin, consider using [`@typescript-eslint/experimental-utils`](../experimental-utils). +If you're building an ESLint plugin, consider using [`@typescript-eslint/experimental-utils`](../experimental-utils) and [`@typescript-eslint/type-utils`](../type-utils). If you're parsing TypeScript code, consider using [`@typescript-eslint/typescript-estree`](../typescript-estree). ## Contributing diff --git a/packages/ast-spec/package.json b/packages/ast-spec/package.json index ccc9be45e274..723eb52795bb 100644 --- a/packages/ast-spec/package.json +++ b/packages/ast-spec/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/ast-spec", - "version": "5.8.1", + "version": "5.9.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 77572fc52ed7..8d95461d9a9e 100644 --- a/packages/eslint-plugin-internal/CHANGELOG.md +++ b/packages/eslint-plugin-internal/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [5.9.0](https://github.com/typescript-eslint/typescript-eslint/compare/v5.8.1...v5.9.0) (2022-01-03) + +**Note:** Version bump only for package @typescript-eslint/eslint-plugin-internal + + + + + ## [5.8.1](https://github.com/typescript-eslint/typescript-eslint/compare/v5.8.0...v5.8.1) (2021-12-27) **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 a443fd6334f4..9ef79b163e6f 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.8.1", + "version": "5.9.0", "private": true, "main": "dist/index.js", "scripts": { @@ -14,8 +14,8 @@ }, "dependencies": { "@types/prettier": "*", - "@typescript-eslint/experimental-utils": "5.8.1", - "@typescript-eslint/scope-manager": "5.8.1", + "@typescript-eslint/experimental-utils": "5.9.0", + "@typescript-eslint/scope-manager": "5.9.0", "prettier": "*" } } diff --git a/packages/eslint-plugin-tslint/CHANGELOG.md b/packages/eslint-plugin-tslint/CHANGELOG.md index cc25451dad13..36b9290f4820 100644 --- a/packages/eslint-plugin-tslint/CHANGELOG.md +++ b/packages/eslint-plugin-tslint/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [5.9.0](https://github.com/typescript-eslint/typescript-eslint/compare/v5.8.1...v5.9.0) (2022-01-03) + +**Note:** Version bump only for package @typescript-eslint/eslint-plugin-tslint + + + + + ## [5.8.1](https://github.com/typescript-eslint/typescript-eslint/compare/v5.8.0...v5.8.1) (2021-12-27) **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 6e6afaf619b6..63979cecb472 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.8.1", + "version": "5.9.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/experimental-utils": "5.8.1", + "@typescript-eslint/experimental-utils": "5.9.0", "lodash": "^4.17.21" }, "peerDependencies": { @@ -48,6 +48,6 @@ }, "devDependencies": { "@types/lodash": "*", - "@typescript-eslint/parser": "5.8.1" + "@typescript-eslint/parser": "5.9.0" } } diff --git a/packages/eslint-plugin/CHANGELOG.md b/packages/eslint-plugin/CHANGELOG.md index 71eef25b4ba7..5f90779b6f93 100644 --- a/packages/eslint-plugin/CHANGELOG.md +++ b/packages/eslint-plugin/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [5.9.0](https://github.com/typescript-eslint/typescript-eslint/compare/v5.8.1...v5.9.0) (2022-01-03) + + +### Features + +* **experimental-utils:** move isTypeReadonly from eslint-plugin to experimental-utils ([#3658](https://github.com/typescript-eslint/typescript-eslint/issues/3658)) ([a9eb0b9](https://github.com/typescript-eslint/typescript-eslint/commit/a9eb0b9eb2db291ea36065ec34f84bf5c5504b43)) + + + + + ## [5.8.1](https://github.com/typescript-eslint/typescript-eslint/compare/v5.8.0...v5.8.1) (2021-12-27) diff --git a/packages/eslint-plugin/docs/rules/no-useless-constructor.md b/packages/eslint-plugin/docs/rules/no-useless-constructor.md index a5ef7f0b664a..07e45ecb0051 100644 --- a/packages/eslint-plugin/docs/rules/no-useless-constructor.md +++ b/packages/eslint-plugin/docs/rules/no-useless-constructor.md @@ -29,6 +29,11 @@ Taken with ❀️ [from ESLint core](https://github.com/eslint/eslint/blob/main/ +## Caveat + +This lint rule will report on constructors whose sole purpose is to change visibility of a parent constructor. +See [discussion on this rule's lack of type information](https://github.com/typescript-eslint/typescript-eslint/issues/3820#issuecomment-917821240) for context. + ## Attributes - [ ] βœ… Recommended diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index d813840aff43..874aae4be62e 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/eslint-plugin", - "version": "5.8.1", + "version": "5.9.0", "description": "TypeScript plugin for ESLint", "keywords": [ "eslint", @@ -44,8 +44,9 @@ "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { - "@typescript-eslint/experimental-utils": "5.8.1", - "@typescript-eslint/scope-manager": "5.8.1", + "@typescript-eslint/experimental-utils": "5.9.0", + "@typescript-eslint/scope-manager": "5.9.0", + "@typescript-eslint/type-utils": "5.9.0", "debug": "^4.3.2", "functional-red-black-tree": "^1.0.1", "ignore": "^5.1.8", diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-boolean-literal-compare.ts b/packages/eslint-plugin/src/rules/no-unnecessary-boolean-literal-compare.ts index 41367d4115b6..4474713b3ff1 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-boolean-literal-compare.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-boolean-literal-compare.ts @@ -158,7 +158,7 @@ export default util.createRule({ function deconstructComparison( node: TSESTree.BinaryExpression, ): BooleanComparison | undefined { - const comparisonType = util.getEqualsKind(node.operator); + const comparisonType = getEqualsKind(node.operator); if (!comparisonType) { return undefined; } @@ -275,3 +275,39 @@ export default util.createRule({ }; }, }); + +interface EqualsKind { + isPositive: boolean; + isStrict: boolean; +} + +function getEqualsKind(operator: string): EqualsKind | undefined { + switch (operator) { + case '==': + return { + isPositive: true, + isStrict: false, + }; + + case '===': + return { + isPositive: true, + isStrict: true, + }; + + case '!=': + return { + isPositive: false, + isStrict: false, + }; + + case '!==': + return { + isPositive: false, + isStrict: true, + }; + + default: + return undefined; + } +} diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts index 4e73c0623ad1..48a3a4d039de 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts @@ -17,6 +17,7 @@ import { createRule, getParserServices, getConstrainedTypeAtLocation, + getTypeOfPropertyOfName, isNullableType, nullThrows, NullThrowsReasons, @@ -24,7 +25,6 @@ import { isTypeAnyType, isTypeUnknownType, getTypeName, - getTypeOfPropertyOfName, } from '../util'; // Truthiness utilities diff --git a/packages/eslint-plugin/src/util/collectUnusedVariables.ts b/packages/eslint-plugin/src/util/collectUnusedVariables.ts index f62d75887537..fe44354fc30d 100644 --- a/packages/eslint-plugin/src/util/collectUnusedVariables.ts +++ b/packages/eslint-plugin/src/util/collectUnusedVariables.ts @@ -3,10 +3,10 @@ import { TSESLint, ASTUtils, TSESTree, + ESLintUtils, } from '@typescript-eslint/experimental-utils'; import { ImplicitLibVariable } from '@typescript-eslint/scope-manager'; import { Visitor } from '@typescript-eslint/scope-manager/dist/referencer/Visitor'; -import { nullThrows } from './nullThrows'; class UnusedVarsVisitor< TMessageIds extends string, @@ -25,7 +25,7 @@ class UnusedVarsVisitor< visitChildrenEvenIfSelectorExists: true, }); - this.#scopeManager = nullThrows( + this.#scopeManager = ESLintUtils.nullThrows( context.getSourceCode().scopeManager, 'Missing required scope manager', ); diff --git a/packages/eslint-plugin/src/util/createRule.ts b/packages/eslint-plugin/src/util/createRule.ts index 8e69adb95659..3d2f75aeba91 100644 --- a/packages/eslint-plugin/src/util/createRule.ts +++ b/packages/eslint-plugin/src/util/createRule.ts @@ -1,10 +1,5 @@ import { ESLintUtils } from '@typescript-eslint/experimental-utils'; -// note - cannot migrate this to an import statement because it will make TSC copy the package.json to the dist folder -// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access -const version: string = require('../../package.json').version; - export const createRule = ESLintUtils.RuleCreator( - name => - `https://github.com/typescript-eslint/typescript-eslint/blob/v${version}/packages/eslint-plugin/docs/rules/${name}.md`, + name => `https://typescript-eslint.io/rules/${name}`, ); diff --git a/packages/eslint-plugin/src/util/explicitReturnTypeUtils.ts b/packages/eslint-plugin/src/util/explicitReturnTypeUtils.ts index fd512749dfb0..c69f8591ae7b 100644 --- a/packages/eslint-plugin/src/util/explicitReturnTypeUtils.ts +++ b/packages/eslint-plugin/src/util/explicitReturnTypeUtils.ts @@ -1,11 +1,11 @@ import { TSESTree, AST_NODE_TYPES, + ESLintUtils, TSESLint, } from '@typescript-eslint/experimental-utils'; import { isTypeAssertion, isConstructor, isSetter } from './astUtils'; import { getFunctionHeadLoc } from './getFunctionHeadLoc'; -import { nullThrows, NullThrowsReasons } from './nullThrows'; type FunctionExpression = | TSESTree.ArrowFunctionExpression @@ -187,7 +187,10 @@ function isTypedFunctionExpression( node: FunctionExpression, options: Options, ): boolean { - const parent = nullThrows(node.parent, NullThrowsReasons.MissingParent); + const parent = ESLintUtils.nullThrows( + node.parent, + ESLintUtils.NullThrowsReasons.MissingParent, + ); if (!options.allowTypedFunctionExpressions) { return false; @@ -215,7 +218,10 @@ function isValidFunctionExpressionReturnType( return true; } - const parent = nullThrows(node.parent, NullThrowsReasons.MissingParent); + const parent = ESLintUtils.nullThrows( + node.parent, + ESLintUtils.NullThrowsReasons.MissingParent, + ); if ( options.allowExpressions && parent.type !== AST_NODE_TYPES.VariableDeclarator && diff --git a/packages/eslint-plugin/src/util/getESLintCoreRule.ts b/packages/eslint-plugin/src/util/getESLintCoreRule.ts index 1701b4ef1d76..7c2427c8fcb9 100644 --- a/packages/eslint-plugin/src/util/getESLintCoreRule.ts +++ b/packages/eslint-plugin/src/util/getESLintCoreRule.ts @@ -1,6 +1,6 @@ +import { ESLintUtils } from '@typescript-eslint/experimental-utils'; import { version } from 'eslint/package.json'; import * as semver from 'semver'; -import { nullThrows } from './nullThrows'; const isESLintV8 = semver.major(version) >= 8; @@ -42,7 +42,7 @@ type RuleId = keyof RuleMap; export const getESLintCoreRule: (ruleId: R) => RuleMap[R] = isESLintV8 ? (ruleId: R): RuleMap[R] => - nullThrows( + ESLintUtils.nullThrows( // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call require('eslint/use-at-your-own-risk').builtinRules.get( ruleId, diff --git a/packages/eslint-plugin/src/util/getWrappingFixer.ts b/packages/eslint-plugin/src/util/getWrappingFixer.ts index d5efa8bfca76..aa01520fdbd5 100644 --- a/packages/eslint-plugin/src/util/getWrappingFixer.ts +++ b/packages/eslint-plugin/src/util/getWrappingFixer.ts @@ -4,7 +4,6 @@ import { ASTUtils, TSESTree, } from '@typescript-eslint/experimental-utils'; -import { SourceCode } from '@typescript-eslint/experimental-utils/src/ts-eslint'; interface WrappingFixerParams { /** Source code. */ @@ -135,7 +134,7 @@ function isWeakPrecedenceParent(node: TSESTree.Node): boolean { */ function isMissingSemicolonBefore( node: TSESTree.Node, - sourceCode: SourceCode, + sourceCode: TSESLint.SourceCode, ): boolean { for (;;) { const parent = node.parent!; diff --git a/packages/eslint-plugin/src/util/index.ts b/packages/eslint-plugin/src/util/index.ts index 4c0284895235..48641babf289 100644 --- a/packages/eslint-plugin/src/util/index.ts +++ b/packages/eslint-plugin/src/util/index.ts @@ -6,17 +6,19 @@ export * from './createRule'; export * from './getFunctionHeadLoc'; export * from './getThisExpression'; export * from './getWrappingFixer'; -export * from './isTypeReadonly'; export * from './misc'; -export * from './nullThrows'; export * from './objectIterators'; -export * from './propertyTypes'; -export * from './requiresQuoting'; -export * from './types'; // this is done for convenience - saves migrating all of the old rules -const { applyDefault, deepMerge, isObjectNotArray, getParserServices } = - ESLintUtils; +export * from '@typescript-eslint/type-utils'; +const { + applyDefault, + deepMerge, + isObjectNotArray, + getParserServices, + nullThrows, + NullThrowsReasons, +} = ESLintUtils; type InferMessageIdsTypeFromRule = ESLintUtils.InferMessageIdsTypeFromRule; type InferOptionsTypeFromRule = ESLintUtils.InferOptionsTypeFromRule; @@ -26,6 +28,8 @@ export { deepMerge, isObjectNotArray, getParserServices, + nullThrows, InferMessageIdsTypeFromRule, InferOptionsTypeFromRule, + NullThrowsReasons, }; diff --git a/packages/eslint-plugin/src/util/misc.ts b/packages/eslint-plugin/src/util/misc.ts index 052e7bfd06ce..4c6b71d2a4de 100644 --- a/packages/eslint-plugin/src/util/misc.ts +++ b/packages/eslint-plugin/src/util/misc.ts @@ -7,7 +7,7 @@ import { TSESLint, TSESTree, } from '@typescript-eslint/experimental-utils'; -import { requiresQuoting } from './requiresQuoting'; +import { requiresQuoting } from '@typescript-eslint/type-utils'; /** * Check if the context file name is *.d.ts or *.d.tsx diff --git a/packages/eslint-plugin/src/util/types.ts b/packages/eslint-plugin/src/util/types.ts deleted file mode 100644 index 089620e702a3..000000000000 --- a/packages/eslint-plugin/src/util/types.ts +++ /dev/null @@ -1,538 +0,0 @@ -import { - AST_NODE_TYPES, - TSESTree, -} from '@typescript-eslint/experimental-utils'; -import debug from 'debug'; -import { - isCallExpression, - isJsxExpression, - isIdentifier, - isNewExpression, - isParameterDeclaration, - isPropertyDeclaration, - isTypeReference, - isUnionOrIntersectionType, - isVariableDeclaration, - unionTypeParts, - isPropertyAssignment, - isBinaryExpression, -} from 'tsutils'; -import * as ts from 'typescript'; - -const log = debug('typescript-eslint:eslint-plugin:utils:types'); - -/** - * Checks if the given type is either an array type, - * or a union made up solely of array types. - */ -export function isTypeArrayTypeOrUnionOfArrayTypes( - type: ts.Type, - checker: ts.TypeChecker, -): boolean { - for (const t of unionTypeParts(type)) { - if (!checker.isArrayType(t)) { - return false; - } - } - - return true; -} - -/** - * @param type Type being checked by name. - * @param allowedNames Symbol names checking on the type. - * @returns Whether the type is, extends, or contains all of the allowed names. - */ -export function containsAllTypesByName( - type: ts.Type, - allowAny: boolean, - allowedNames: Set, -): boolean { - if (isTypeFlagSet(type, ts.TypeFlags.Any | ts.TypeFlags.Unknown)) { - return !allowAny; - } - - if (isTypeReference(type)) { - type = type.target; - } - - const symbol = type.getSymbol(); - if (symbol && allowedNames.has(symbol.name)) { - return true; - } - - if (isUnionOrIntersectionType(type)) { - return type.types.every(t => - containsAllTypesByName(t, allowAny, allowedNames), - ); - } - - const bases = type.getBaseTypes(); - return ( - typeof bases !== 'undefined' && - bases.length > 0 && - bases.every(t => containsAllTypesByName(t, allowAny, allowedNames)) - ); -} - -/** - * Get the type name of a given type. - * @param typeChecker The context sensitive TypeScript TypeChecker. - * @param type The type to get the name of. - */ -export function getTypeName( - typeChecker: ts.TypeChecker, - type: ts.Type, -): string { - // It handles `string` and string literal types as string. - if ((type.flags & ts.TypeFlags.StringLike) !== 0) { - return 'string'; - } - - // If the type is a type parameter which extends primitive string types, - // but it was not recognized as a string like. So check the constraint - // type of the type parameter. - if ((type.flags & ts.TypeFlags.TypeParameter) !== 0) { - // `type.getConstraint()` method doesn't return the constraint type of - // the type parameter for some reason. So this gets the constraint type - // via AST. - const symbol = type.getSymbol(); - const decls = symbol?.getDeclarations(); - const typeParamDecl = decls?.[0] as ts.TypeParameterDeclaration; - if ( - ts.isTypeParameterDeclaration(typeParamDecl) && - typeParamDecl.constraint != null - ) { - return getTypeName( - typeChecker, - typeChecker.getTypeFromTypeNode(typeParamDecl.constraint), - ); - } - } - - // If the type is a union and all types in the union are string like, - // return `string`. For example: - // - `"a" | "b"` is string. - // - `string | string[]` is not string. - if ( - type.isUnion() && - type.types - .map(value => getTypeName(typeChecker, value)) - .every(t => t === 'string') - ) { - return 'string'; - } - - // If the type is an intersection and a type in the intersection is string - // like, return `string`. For example: `string & {__htmlEscaped: void}` - if ( - type.isIntersection() && - type.types - .map(value => getTypeName(typeChecker, value)) - .some(t => t === 'string') - ) { - return 'string'; - } - - return typeChecker.typeToString(type); -} - -/** - * Resolves the given node's type. Will resolve to the type's generic constraint, if it has one. - */ -export function getConstrainedTypeAtLocation( - checker: ts.TypeChecker, - node: ts.Node, -): ts.Type { - const nodeType = checker.getTypeAtLocation(node); - const constrained = checker.getBaseConstraintOfType(nodeType); - - return constrained ?? nodeType; -} - -/** - * Checks if the given type is (or accepts) nullable - * @param isReceiver true if the type is a receiving type (i.e. the type of a called function's parameter) - */ -export function isNullableType( - type: ts.Type, - { - isReceiver = false, - allowUndefined = true, - }: { isReceiver?: boolean; allowUndefined?: boolean } = {}, -): boolean { - const flags = getTypeFlags(type); - - if (isReceiver && flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) { - return true; - } - - if (allowUndefined) { - return (flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) !== 0; - } else { - return (flags & ts.TypeFlags.Null) !== 0; - } -} - -/** - * Gets the declaration for the given variable - */ -export function getDeclaration( - checker: ts.TypeChecker, - node: ts.Expression, -): ts.Declaration | null { - const symbol = checker.getSymbolAtLocation(node); - if (!symbol) { - return null; - } - const declarations = symbol.getDeclarations(); - return declarations?.[0] ?? null; -} - -/** - * Gets all of the type flags in a type, iterating through unions automatically - */ -export function getTypeFlags(type: ts.Type): ts.TypeFlags { - let flags: ts.TypeFlags = 0; - for (const t of unionTypeParts(type)) { - flags |= t.flags; - } - return flags; -} - -/** - * Checks if the given type is (or accepts) the given flags - * @param isReceiver true if the type is a receiving type (i.e. the type of a called function's parameter) - */ -export function isTypeFlagSet( - type: ts.Type, - flagsToCheck: ts.TypeFlags, - isReceiver?: boolean, -): boolean { - const flags = getTypeFlags(type); - - if (isReceiver && flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) { - return true; - } - - return (flags & flagsToCheck) !== 0; -} - -/** - * @returns Whether a type is an instance of the parent type, including for the parent's base types. - */ -export function typeIsOrHasBaseType( - type: ts.Type, - parentType: ts.Type, -): boolean { - const parentSymbol = parentType.getSymbol(); - if (!type.getSymbol() || !parentSymbol) { - return false; - } - - const typeAndBaseTypes = [type]; - const ancestorTypes = type.getBaseTypes(); - - if (ancestorTypes) { - typeAndBaseTypes.push(...ancestorTypes); - } - - for (const baseType of typeAndBaseTypes) { - const baseSymbol = baseType.getSymbol(); - if (baseSymbol && baseSymbol.name === parentSymbol.name) { - return true; - } - } - - return false; -} - -/** - * Gets the source file for a given node - */ -export function getSourceFileOfNode(node: ts.Node): ts.SourceFile { - while (node && node.kind !== ts.SyntaxKind.SourceFile) { - node = node.parent; - } - return node as ts.SourceFile; -} - -export function getTokenAtPosition( - sourceFile: ts.SourceFile, - position: number, -): ts.Node { - const queue: ts.Node[] = [sourceFile]; - let current: ts.Node; - while (queue.length > 0) { - current = queue.shift()!; - // find the child that contains 'position' - for (const child of current.getChildren(sourceFile)) { - const start = child.getFullStart(); - if (start > position) { - // If this child begins after position, then all subsequent children will as well. - return current; - } - - const end = child.getEnd(); - if ( - position < end || - (position === end && child.kind === ts.SyntaxKind.EndOfFileToken) - ) { - queue.push(child); - break; - } - } - } - return current!; -} - -export interface EqualsKind { - isPositive: boolean; - isStrict: boolean; -} - -export function getEqualsKind(operator: string): EqualsKind | undefined { - switch (operator) { - case '==': - return { - isPositive: true, - isStrict: false, - }; - - case '===': - return { - isPositive: true, - isStrict: true, - }; - - case '!=': - return { - isPositive: false, - isStrict: false, - }; - - case '!==': - return { - isPositive: false, - isStrict: true, - }; - - default: - return undefined; - } -} - -export function getTypeArguments( - type: ts.TypeReference, - checker: ts.TypeChecker, -): readonly ts.Type[] { - // getTypeArguments was only added in TS3.7 - if (checker.getTypeArguments) { - return checker.getTypeArguments(type); - } - - return type.typeArguments ?? []; -} - -/** - * @returns true if the type is `unknown` - */ -export function isTypeUnknownType(type: ts.Type): boolean { - return isTypeFlagSet(type, ts.TypeFlags.Unknown); -} - -/** - * @returns true if the type is `any` - */ -export function isTypeAnyType(type: ts.Type): boolean { - if (isTypeFlagSet(type, ts.TypeFlags.Any)) { - if (type.intrinsicName === 'error') { - log('Found an "error" any type'); - } - return true; - } - return false; -} - -/** - * @returns true if the type is `any[]` - */ -export function isTypeAnyArrayType( - type: ts.Type, - checker: ts.TypeChecker, -): boolean { - return ( - checker.isArrayType(type) && - isTypeAnyType( - // getTypeArguments was only added in TS3.7 - getTypeArguments(type, checker)[0], - ) - ); -} - -/** - * @returns true if the type is `unknown[]` - */ -export function isTypeUnknownArrayType( - type: ts.Type, - checker: ts.TypeChecker, -): boolean { - return ( - checker.isArrayType(type) && - isTypeUnknownType( - // getTypeArguments was only added in TS3.7 - getTypeArguments(type, checker)[0], - ) - ); -} - -export const enum AnyType { - Any, - AnyArray, - Safe, -} -/** - * @returns `AnyType.Any` if the type is `any`, `AnyType.AnyArray` if the type is `any[]` or `readonly any[]`, - * otherwise it returns `AnyType.Safe`. - */ -export function isAnyOrAnyArrayTypeDiscriminated( - node: ts.Node, - checker: ts.TypeChecker, -): AnyType { - const type = checker.getTypeAtLocation(node); - if (isTypeAnyType(type)) { - return AnyType.Any; - } - if (isTypeAnyArrayType(type, checker)) { - return AnyType.AnyArray; - } - return AnyType.Safe; -} - -/** - * Does a simple check to see if there is an any being assigned to a non-any type. - * - * This also checks generic positions to ensure there's no unsafe sub-assignments. - * Note: in the case of generic positions, it makes the assumption that the two types are the same. - * - * @example See tests for examples - * - * @returns false if it's safe, or an object with the two types if it's unsafe - */ -export function isUnsafeAssignment( - type: ts.Type, - receiver: ts.Type, - checker: ts.TypeChecker, - senderNode: TSESTree.Node | null, -): false | { sender: ts.Type; receiver: ts.Type } { - if (isTypeAnyType(type)) { - // Allow assignment of any ==> unknown. - if (isTypeUnknownType(receiver)) { - return false; - } - - if (!isTypeAnyType(receiver)) { - return { sender: type, receiver }; - } - } - - if (isTypeReference(type) && isTypeReference(receiver)) { - // TODO - figure out how to handle cases like this, - // where the types are assignable, but not the same type - /* - function foo(): ReadonlySet { return new Set(); } - - // and - - type Test = { prop: T } - type Test2 = { prop: string } - declare const a: Test; - const b: Test2 = a; - */ - - if (type.target !== receiver.target) { - // if the type references are different, assume safe, as we won't know how to compare the two types - // the generic positions might not be equivalent for both types - return false; - } - - if ( - senderNode?.type === AST_NODE_TYPES.NewExpression && - senderNode.callee.type === AST_NODE_TYPES.Identifier && - senderNode.callee.name === 'Map' && - senderNode.arguments.length === 0 && - senderNode.typeParameters == null - ) { - // special case to handle `new Map()` - // unfortunately Map's default empty constructor is typed to return `Map` :( - // https://github.com/typescript-eslint/typescript-eslint/issues/2109#issuecomment-634144396 - return false; - } - - const typeArguments = type.typeArguments ?? []; - const receiverTypeArguments = receiver.typeArguments ?? []; - - for (let i = 0; i < typeArguments.length; i += 1) { - const arg = typeArguments[i]; - const receiverArg = receiverTypeArguments[i]; - - const unsafe = isUnsafeAssignment(arg, receiverArg, checker, senderNode); - if (unsafe) { - return { sender: type, receiver }; - } - } - - return false; - } - - return false; -} - -/** - * Returns the contextual type of a given node. - * Contextual type is the type of the target the node is going into. - * i.e. the type of a called function's parameter, or the defined type of a variable declaration - */ -export function getContextualType( - checker: ts.TypeChecker, - node: ts.Expression, -): ts.Type | undefined { - const parent = node.parent; - if (!parent) { - return; - } - - if (isCallExpression(parent) || isNewExpression(parent)) { - if (node === parent.expression) { - // is the callee, so has no contextual type - return; - } - } else if ( - isVariableDeclaration(parent) || - isPropertyDeclaration(parent) || - isParameterDeclaration(parent) - ) { - return parent.type ? checker.getTypeFromTypeNode(parent.type) : undefined; - } else if (isJsxExpression(parent)) { - return checker.getContextualType(parent); - } else if (isPropertyAssignment(parent) && isIdentifier(node)) { - return checker.getContextualType(node); - } else if ( - isBinaryExpression(parent) && - parent.operatorToken.kind === ts.SyntaxKind.EqualsToken && - parent.right === node - ) { - // is RHS of assignment - return checker.getTypeAtLocation(parent.left); - } else if ( - ![ts.SyntaxKind.TemplateSpan, ts.SyntaxKind.JsxExpression].includes( - parent.kind, - ) - ) { - // parent is not something we know we can get the contextual type of - return; - } - // TODO - support return statement checking - - return checker.getContextualType(node); -} diff --git a/packages/eslint-plugin/tests/docs.test.ts b/packages/eslint-plugin/tests/docs.test.ts index 61487b5169f0..5200bb5f3ba8 100644 --- a/packages/eslint-plugin/tests/docs.test.ts +++ b/packages/eslint-plugin/tests/docs.test.ts @@ -118,9 +118,9 @@ describe('Validating rule metadata', () => { it('`name` field in rule must match the filename', () => { // validate if rule name is same as url // there is no way to access this field but its used only in generation of docs url - expect( - rule.meta.docs?.url?.endsWith(`rules/${ruleName}.md`), - ).toBeTruthy(); + expect(rule.meta.docs?.url).toBe( + `https://typescript-eslint.io/rules/${ruleName}`, + ); }); it('`requiresTypeChecking` should be set if the rule uses type information', () => { diff --git a/packages/eslint-plugin/tsconfig.build.json b/packages/eslint-plugin/tsconfig.build.json index bc597c33129b..fc60d9a85a78 100644 --- a/packages/eslint-plugin/tsconfig.build.json +++ b/packages/eslint-plugin/tsconfig.build.json @@ -12,6 +12,7 @@ "references": [ { "path": "../experimental-utils/tsconfig.build.json" }, { "path": "../parser/tsconfig.build.json" }, - { "path": "../scope-manager/tsconfig.build.json" } + { "path": "../scope-manager/tsconfig.build.json" }, + { "path": "../type-utils/tsconfig.build.json" } ] } diff --git a/packages/eslint-plugin/tsconfig.json b/packages/eslint-plugin/tsconfig.json index c7e9c4ecb2b7..31db855dad82 100644 --- a/packages/eslint-plugin/tsconfig.json +++ b/packages/eslint-plugin/tsconfig.json @@ -8,6 +8,7 @@ "references": [ { "path": "../experimental-utils/tsconfig.build.json" }, { "path": "../parser/tsconfig.build.json" }, - { "path": "../scope-manager/tsconfig.build.json" } + { "path": "../scope-manager/tsconfig.build.json" }, + { "path": "../type-utils/tsconfig.build.json" } ] } diff --git a/packages/eslint-plugin/typings/typescript.d.ts b/packages/eslint-plugin/typings/typescript.d.ts index 73304155ee74..0f0b38a5ba6f 100644 --- a/packages/eslint-plugin/typings/typescript.d.ts +++ b/packages/eslint-plugin/typings/typescript.d.ts @@ -18,16 +18,5 @@ declare module 'typescript' { * - `readonly [foo]` */ isTupleType(type: Type): type is TupleTypeReference; - /** - * Return the type of the given property in the given type, or undefined if no such property exists - */ - getTypeOfPropertyOfType(type: Type, propertyName: string): Type | undefined; - } - - interface Type { - /** - * If the type is `any`, and this is set to "error", then TS was unable to resolve the type - */ - intrinsicName?: string; } } diff --git a/packages/experimental-utils/CHANGELOG.md b/packages/experimental-utils/CHANGELOG.md index 9a67454c04b5..d3d5956b78ec 100644 --- a/packages/experimental-utils/CHANGELOG.md +++ b/packages/experimental-utils/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [5.9.0](https://github.com/typescript-eslint/typescript-eslint/compare/v5.8.1...v5.9.0) (2022-01-03) + + +### Features + +* **experimental-utils:** move isTypeReadonly from eslint-plugin to experimental-utils ([#3658](https://github.com/typescript-eslint/typescript-eslint/issues/3658)) ([a9eb0b9](https://github.com/typescript-eslint/typescript-eslint/commit/a9eb0b9eb2db291ea36065ec34f84bf5c5504b43)) + + + + + ## [5.8.1](https://github.com/typescript-eslint/typescript-eslint/compare/v5.8.0...v5.8.1) (2021-12-27) **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 0842982c8670..0180beeeffad 100644 --- a/packages/experimental-utils/package.json +++ b/packages/experimental-utils/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/experimental-utils", - "version": "5.8.1", + "version": "5.9.0", "description": "(Experimental) Utilities for working with TypeScript + ESLint together", "keywords": [ "eslint", @@ -40,9 +40,9 @@ }, "dependencies": { "@types/json-schema": "^7.0.9", - "@typescript-eslint/scope-manager": "5.8.1", - "@typescript-eslint/types": "5.8.1", - "@typescript-eslint/typescript-estree": "5.8.1", + "@typescript-eslint/scope-manager": "5.9.0", + "@typescript-eslint/types": "5.9.0", + "@typescript-eslint/typescript-estree": "5.9.0", "eslint-scope": "^5.1.1", "eslint-utils": "^3.0.0" }, diff --git a/packages/experimental-utils/src/eslint-utils/index.ts b/packages/experimental-utils/src/eslint-utils/index.ts index bbbe8df2709f..fc8e410428e6 100644 --- a/packages/experimental-utils/src/eslint-utils/index.ts +++ b/packages/experimental-utils/src/eslint-utils/index.ts @@ -5,3 +5,4 @@ export * from './InferTypesFromRule'; export * from './RuleCreator'; export * from './RuleTester'; export * from './deepMerge'; +export * from './nullThrows'; diff --git a/packages/eslint-plugin/src/util/nullThrows.ts b/packages/experimental-utils/src/eslint-utils/nullThrows.ts similarity index 100% rename from packages/eslint-plugin/src/util/nullThrows.ts rename to packages/experimental-utils/src/eslint-utils/nullThrows.ts diff --git a/packages/parser/CHANGELOG.md b/packages/parser/CHANGELOG.md index f4133fca9cd8..89cd4b112b96 100644 --- a/packages/parser/CHANGELOG.md +++ b/packages/parser/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [5.9.0](https://github.com/typescript-eslint/typescript-eslint/compare/v5.8.1...v5.9.0) (2022-01-03) + +**Note:** Version bump only for package @typescript-eslint/parser + + + + + ## [5.8.1](https://github.com/typescript-eslint/typescript-eslint/compare/v5.8.0...v5.8.1) (2021-12-27) **Note:** Version bump only for package @typescript-eslint/parser diff --git a/packages/parser/package.json b/packages/parser/package.json index 4bb03576b7c8..8d71c5e96cff 100644 --- a/packages/parser/package.json +++ b/packages/parser/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/parser", - "version": "5.8.1", + "version": "5.9.0", "description": "An ESLint custom parser which leverages TypeScript ESTree", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -44,9 +44,9 @@ "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "dependencies": { - "@typescript-eslint/scope-manager": "5.8.1", - "@typescript-eslint/types": "5.8.1", - "@typescript-eslint/typescript-estree": "5.8.1", + "@typescript-eslint/scope-manager": "5.9.0", + "@typescript-eslint/types": "5.9.0", + "@typescript-eslint/typescript-estree": "5.9.0", "debug": "^4.3.2" }, "devDependencies": { diff --git a/packages/scope-manager/CHANGELOG.md b/packages/scope-manager/CHANGELOG.md index cf01cabe6dbd..d1cc19638c44 100644 --- a/packages/scope-manager/CHANGELOG.md +++ b/packages/scope-manager/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [5.9.0](https://github.com/typescript-eslint/typescript-eslint/compare/v5.8.1...v5.9.0) (2022-01-03) + +**Note:** Version bump only for package @typescript-eslint/scope-manager + + + + + ## [5.8.1](https://github.com/typescript-eslint/typescript-eslint/compare/v5.8.0...v5.8.1) (2021-12-27) **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 64fdb12ceedd..d8568675331f 100644 --- a/packages/scope-manager/package.json +++ b/packages/scope-manager/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/scope-manager", - "version": "5.8.1", + "version": "5.9.0", "description": "TypeScript scope analyser for ESLint", "keywords": [ "eslint", @@ -39,12 +39,12 @@ "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { - "@typescript-eslint/types": "5.8.1", - "@typescript-eslint/visitor-keys": "5.8.1" + "@typescript-eslint/types": "5.9.0", + "@typescript-eslint/visitor-keys": "5.9.0" }, "devDependencies": { "@types/glob": "*", - "@typescript-eslint/typescript-estree": "5.8.1", + "@typescript-eslint/typescript-estree": "5.9.0", "glob": "*", "jest-specific-snapshot": "*", "make-dir": "*", diff --git a/packages/shared-fixtures/CHANGELOG.md b/packages/shared-fixtures/CHANGELOG.md index e0450e58c48e..16f1b28315f1 100644 --- a/packages/shared-fixtures/CHANGELOG.md +++ b/packages/shared-fixtures/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [5.9.0](https://github.com/typescript-eslint/typescript-eslint/compare/v5.8.1...v5.9.0) (2022-01-03) + +**Note:** Version bump only for package @typescript-eslint/shared-fixtures + + + + + ## [5.8.1](https://github.com/typescript-eslint/typescript-eslint/compare/v5.8.0...v5.8.1) (2021-12-27) **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 d957d2677a22..cfc2e216a498 100644 --- a/packages/shared-fixtures/package.json +++ b/packages/shared-fixtures/package.json @@ -1,5 +1,5 @@ { "name": "@typescript-eslint/shared-fixtures", - "version": "5.8.1", + "version": "5.9.0", "private": true } diff --git a/packages/type-utils/CHANGELOG.md b/packages/type-utils/CHANGELOG.md new file mode 100644 index 000000000000..37da75eeafa3 --- /dev/null +++ b/packages/type-utils/CHANGELOG.md @@ -0,0 +1,11 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [5.9.0](https://github.com/typescript-eslint/typescript-eslint/compare/v5.8.1...v5.9.0) (2022-01-03) + + +### Features + +* **experimental-utils:** move isTypeReadonly from eslint-plugin to experimental-utils ([#3658](https://github.com/typescript-eslint/typescript-eslint/issues/3658)) ([a9eb0b9](https://github.com/typescript-eslint/typescript-eslint/commit/a9eb0b9eb2db291ea36065ec34f84bf5c5504b43)) diff --git a/packages/type-utils/LICENSE b/packages/type-utils/LICENSE new file mode 100644 index 000000000000..7641edcfd0af --- /dev/null +++ b/packages/type-utils/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 TypeScript ESLint and other contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/type-utils/README.md b/packages/type-utils/README.md new file mode 100644 index 000000000000..099067095a41 --- /dev/null +++ b/packages/type-utils/README.md @@ -0,0 +1,15 @@ +

Type utils for ESLint Plugins

+ +

Type utilities for working with TypeScript within ESLint rules.

+ +

+ CI + NPM Version + NPM Downloads +

+ +This utilities in this package are separated from `@typescript-eslint/experimental-utils` so that that package does not require a dependency on `typescript`. + +## Contributing + +[See the contributing guide here](../../CONTRIBUTING.md) diff --git a/packages/type-utils/jest.config.js b/packages/type-utils/jest.config.js new file mode 100644 index 000000000000..bf4e270e3760 --- /dev/null +++ b/packages/type-utils/jest.config.js @@ -0,0 +1,21 @@ +'use strict'; + +// @ts-check +/** @type {import('@jest/types').Config.InitialOptions} */ +module.exports = { + resolver: '/../../tests/jest-resolver.js', + globals: { + 'ts-jest': { + isolatedModules: true, + }, + }, + testEnvironment: 'node', + transform: { + '^.+\\.tsx?$': 'ts-jest', + }, + testRegex: './tests/.+\\.test\\.ts$', + collectCoverage: false, + collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}'], + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], + coverageReporters: ['text-summary', 'lcov'], +}; diff --git a/packages/type-utils/package.json b/packages/type-utils/package.json new file mode 100644 index 000000000000..d5db1684001c --- /dev/null +++ b/packages/type-utils/package.json @@ -0,0 +1,69 @@ +{ + "name": "@typescript-eslint/type-utils", + "version": "5.9.0", + "description": "Type utilities for working with TypeScript + ESLint together", + "keywords": [ + "eslint", + "typescript", + "estree" + ], + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "files": [ + "dist", + "_ts3.4", + "package.json", + "README.md", + "LICENSE" + ], + "repository": { + "type": "git", + "url": "https://github.com/typescript-eslint/typescript-eslint.git", + "directory": "packages/type-utils" + }, + "bugs": { + "url": "https://github.com/typescript-eslint/typescript-eslint/issues" + }, + "license": "MIT", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc -b tsconfig.build.json", + "postbuild": "downlevel-dts dist _ts3.4/dist", + "clean": "tsc -b tsconfig.build.json --clean", + "postclean": "rimraf dist && rimraf _ts3.4 && rimraf coverage", + "format": "prettier --write \"./**/*.{ts,js,json,md}\" --ignore-path ../../.prettierignore", + "lint": "eslint . --ext .js,.ts --ignore-path='../../.eslintignore'", + "test": "jest --coverage", + "typecheck": "tsc -p tsconfig.json --noEmit" + }, + "dependencies": { + "@typescript-eslint/experimental-utils": "5.9.0", + "debug": "^4.3.2", + "tsutils": "^3.21.0" + }, + "devDependencies": { + "@typescript-eslint/parser": "5.9.0", + "typescript": "*" + }, + "peerDependencies": { + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "typesVersions": { + "<3.8": { + "*": [ + "_ts3.4/*" + ] + } + } +} diff --git a/packages/type-utils/project.json b/packages/type-utils/project.json new file mode 100644 index 000000000000..fea4ee94ee4d --- /dev/null +++ b/packages/type-utils/project.json @@ -0,0 +1,5 @@ +{ + "root": "packages/type-utils", + "type": "library", + "implicitDependencies": [] +} diff --git a/packages/type-utils/src/containsAllTypesByName.ts b/packages/type-utils/src/containsAllTypesByName.ts new file mode 100644 index 000000000000..798355adb593 --- /dev/null +++ b/packages/type-utils/src/containsAllTypesByName.ts @@ -0,0 +1,40 @@ +import { isTypeReference, isUnionOrIntersectionType } from 'tsutils'; +import * as ts from 'typescript'; +import { isTypeFlagSet } from './typeFlagUtils'; + +/** + * @param type Type being checked by name. + * @param allowedNames Symbol names checking on the type. + * @returns Whether the type is, extends, or contains all of the allowed names. + */ +export function containsAllTypesByName( + type: ts.Type, + allowAny: boolean, + allowedNames: Set, +): boolean { + if (isTypeFlagSet(type, ts.TypeFlags.Any | ts.TypeFlags.Unknown)) { + return !allowAny; + } + + if (isTypeReference(type)) { + type = type.target; + } + + const symbol = type.getSymbol(); + if (symbol && allowedNames.has(symbol.name)) { + return true; + } + + if (isUnionOrIntersectionType(type)) { + return type.types.every(t => + containsAllTypesByName(t, allowAny, allowedNames), + ); + } + + const bases = type.getBaseTypes(); + return ( + typeof bases !== 'undefined' && + bases.length > 0 && + bases.every(t => containsAllTypesByName(t, allowAny, allowedNames)) + ); +} diff --git a/packages/type-utils/src/getConstrainedTypeAtLocation.ts b/packages/type-utils/src/getConstrainedTypeAtLocation.ts new file mode 100644 index 000000000000..d14b4d4a7bff --- /dev/null +++ b/packages/type-utils/src/getConstrainedTypeAtLocation.ts @@ -0,0 +1,14 @@ +import * as ts from 'typescript'; + +/** + * Resolves the given node's type. Will resolve to the type's generic constraint, if it has one. + */ +export function getConstrainedTypeAtLocation( + checker: ts.TypeChecker, + node: ts.Node, +): ts.Type { + const nodeType = checker.getTypeAtLocation(node); + const constrained = checker.getBaseConstraintOfType(nodeType); + + return constrained ?? nodeType; +} diff --git a/packages/type-utils/src/getContextualType.ts b/packages/type-utils/src/getContextualType.ts new file mode 100644 index 000000000000..78bffbc523cb --- /dev/null +++ b/packages/type-utils/src/getContextualType.ts @@ -0,0 +1,61 @@ +import { + isCallExpression, + isJsxExpression, + isIdentifier, + isNewExpression, + isParameterDeclaration, + isPropertyDeclaration, + isVariableDeclaration, + isPropertyAssignment, + isBinaryExpression, +} from 'tsutils'; +import * as ts from 'typescript'; + +/** + * Returns the contextual type of a given node. + * Contextual type is the type of the target the node is going into. + * i.e. the type of a called function's parameter, or the defined type of a variable declaration + */ +export function getContextualType( + checker: ts.TypeChecker, + node: ts.Expression, +): ts.Type | undefined { + const parent = node.parent; + if (!parent) { + return; + } + + if (isCallExpression(parent) || isNewExpression(parent)) { + if (node === parent.expression) { + // is the callee, so has no contextual type + return; + } + } else if ( + isVariableDeclaration(parent) || + isPropertyDeclaration(parent) || + isParameterDeclaration(parent) + ) { + return parent.type ? checker.getTypeFromTypeNode(parent.type) : undefined; + } else if (isJsxExpression(parent)) { + return checker.getContextualType(parent); + } else if (isPropertyAssignment(parent) && isIdentifier(node)) { + return checker.getContextualType(node); + } else if ( + isBinaryExpression(parent) && + parent.operatorToken.kind === ts.SyntaxKind.EqualsToken && + parent.right === node + ) { + // is RHS of assignment + return checker.getTypeAtLocation(parent.left); + } else if ( + ![ts.SyntaxKind.TemplateSpan, ts.SyntaxKind.JsxExpression].includes( + parent.kind, + ) + ) { + // parent is not something we know we can get the contextual type of + return; + } + // TODO - support return statement checking + + return checker.getContextualType(node); +} diff --git a/packages/type-utils/src/getDeclaration.ts b/packages/type-utils/src/getDeclaration.ts new file mode 100644 index 000000000000..6dab23f92cd3 --- /dev/null +++ b/packages/type-utils/src/getDeclaration.ts @@ -0,0 +1,16 @@ +import * as ts from 'typescript'; + +/** + * Gets the declaration for the given variable + */ +export function getDeclaration( + checker: ts.TypeChecker, + node: ts.Expression, +): ts.Declaration | null { + const symbol = checker.getSymbolAtLocation(node); + if (!symbol) { + return null; + } + const declarations = symbol.getDeclarations(); + return declarations?.[0] ?? null; +} diff --git a/packages/type-utils/src/getSourceFileOfNode.ts b/packages/type-utils/src/getSourceFileOfNode.ts new file mode 100644 index 000000000000..9bd8f77ea464 --- /dev/null +++ b/packages/type-utils/src/getSourceFileOfNode.ts @@ -0,0 +1,11 @@ +import * as ts from 'typescript'; + +/** + * Gets the source file for a given node + */ +export function getSourceFileOfNode(node: ts.Node): ts.SourceFile { + while (node && node.kind !== ts.SyntaxKind.SourceFile) { + node = node.parent; + } + return node as ts.SourceFile; +} diff --git a/packages/type-utils/src/getTokenAtPosition.ts b/packages/type-utils/src/getTokenAtPosition.ts new file mode 100644 index 000000000000..9307a9cf5c40 --- /dev/null +++ b/packages/type-utils/src/getTokenAtPosition.ts @@ -0,0 +1,30 @@ +import * as ts from 'typescript'; + +export function getTokenAtPosition( + sourceFile: ts.SourceFile, + position: number, +): ts.Node { + const queue: ts.Node[] = [sourceFile]; + let current: ts.Node; + while (queue.length > 0) { + current = queue.shift()!; + // find the child that contains 'position' + for (const child of current.getChildren(sourceFile)) { + const start = child.getFullStart(); + if (start > position) { + // If this child begins after position, then all subsequent children will as well. + return current; + } + + const end = child.getEnd(); + if ( + position < end || + (position === end && child.kind === ts.SyntaxKind.EndOfFileToken) + ) { + queue.push(child); + break; + } + } + } + return current!; +} diff --git a/packages/type-utils/src/getTypeArguments.ts b/packages/type-utils/src/getTypeArguments.ts new file mode 100644 index 000000000000..5af50f81d337 --- /dev/null +++ b/packages/type-utils/src/getTypeArguments.ts @@ -0,0 +1,13 @@ +import * as ts from 'typescript'; + +export function getTypeArguments( + type: ts.TypeReference, + checker: ts.TypeChecker, +): readonly ts.Type[] { + // getTypeArguments was only added in TS3.7 + if (checker.getTypeArguments) { + return checker.getTypeArguments(type); + } + + return type.typeArguments ?? []; +} diff --git a/packages/type-utils/src/getTypeName.ts b/packages/type-utils/src/getTypeName.ts new file mode 100644 index 000000000000..ea3f69e4b5d6 --- /dev/null +++ b/packages/type-utils/src/getTypeName.ts @@ -0,0 +1,63 @@ +import * as ts from 'typescript'; + +/** + * Get the type name of a given type. + * @param typeChecker The context sensitive TypeScript TypeChecker. + * @param type The type to get the name of. + */ +export function getTypeName( + typeChecker: ts.TypeChecker, + type: ts.Type, +): string { + // It handles `string` and string literal types as string. + if ((type.flags & ts.TypeFlags.StringLike) !== 0) { + return 'string'; + } + + // If the type is a type parameter which extends primitive string types, + // but it was not recognized as a string like. So check the constraint + // type of the type parameter. + if ((type.flags & ts.TypeFlags.TypeParameter) !== 0) { + // `type.getConstraint()` method doesn't return the constraint type of + // the type parameter for some reason. So this gets the constraint type + // via AST. + const symbol = type.getSymbol(); + const decls = symbol?.getDeclarations(); + const typeParamDecl = decls?.[0] as ts.TypeParameterDeclaration; + if ( + ts.isTypeParameterDeclaration(typeParamDecl) && + typeParamDecl.constraint != null + ) { + return getTypeName( + typeChecker, + typeChecker.getTypeFromTypeNode(typeParamDecl.constraint), + ); + } + } + + // If the type is a union and all types in the union are string like, + // return `string`. For example: + // - `"a" | "b"` is string. + // - `string | string[]` is not string. + if ( + type.isUnion() && + type.types + .map(value => getTypeName(typeChecker, value)) + .every(t => t === 'string') + ) { + return 'string'; + } + + // If the type is an intersection and a type in the intersection is string + // like, return `string`. For example: `string & {__htmlEscaped: void}` + if ( + type.isIntersection() && + type.types + .map(value => getTypeName(typeChecker, value)) + .some(t => t === 'string') + ) { + return 'string'; + } + + return typeChecker.typeToString(type); +} diff --git a/packages/type-utils/src/index.ts b/packages/type-utils/src/index.ts new file mode 100644 index 000000000000..44eb35ec945b --- /dev/null +++ b/packages/type-utils/src/index.ts @@ -0,0 +1,14 @@ +export * from './containsAllTypesByName'; +export * from './getConstrainedTypeAtLocation'; +export * from './getContextualType'; +export * from './getDeclaration'; +export * from './getSourceFileOfNode'; +export * from './getTokenAtPosition'; +export * from './getTypeArguments'; +export * from './getTypeName'; +export * from './isTypeReadonly'; +export * from './isUnsafeAssignment'; +export * from './predicates'; +export * from './propertyTypes'; +export * from './requiresQuoting'; +export * from './typeFlagUtils'; diff --git a/packages/eslint-plugin/src/util/isTypeReadonly.ts b/packages/type-utils/src/isTypeReadonly.ts similarity index 94% rename from packages/eslint-plugin/src/util/isTypeReadonly.ts rename to packages/type-utils/src/isTypeReadonly.ts index efb6966d6766..c6bc0f5761a4 100644 --- a/packages/eslint-plugin/src/util/isTypeReadonly.ts +++ b/packages/type-utils/src/isTypeReadonly.ts @@ -1,3 +1,4 @@ +import { ESLintUtils } from '@typescript-eslint/experimental-utils'; import { isObjectType, isUnionType, @@ -7,7 +8,6 @@ import { isSymbolFlagSet, } from 'tsutils'; import * as ts from 'typescript'; -import { nullThrows, NullThrowsReasons } from './nullThrows'; import { getTypeOfPropertyOfType } from './propertyTypes'; const enum Readonlyness { @@ -75,9 +75,9 @@ function isTypeReadonlyArrayOrTuple( } if (checker.isArrayType(type)) { - const symbol = nullThrows( + const symbol = ESLintUtils.nullThrows( type.getSymbol(), - NullThrowsReasons.MissingToken('symbol', 'array type'), + ESLintUtils.NullThrowsReasons.MissingToken('symbol', 'array type'), ); const escapedName = symbol.getEscapedName(); if (escapedName === 'Array') { @@ -142,9 +142,12 @@ function isTypeReadonlyObject( // as we might be able to bail out early due to a mutable property before // doing this deep, potentially expensive check. for (const property of properties) { - const propertyType = nullThrows( + const propertyType = ESLintUtils.nullThrows( getTypeOfPropertyOfType(checker, type, property), - NullThrowsReasons.MissingToken(`property "${property.name}"`, 'type'), + ESLintUtils.NullThrowsReasons.MissingToken( + `property "${property.name}"`, + 'type', + ), ); // handle recursive types. diff --git a/packages/type-utils/src/isUnsafeAssignment.ts b/packages/type-utils/src/isUnsafeAssignment.ts new file mode 100644 index 000000000000..32dc84b0c306 --- /dev/null +++ b/packages/type-utils/src/isUnsafeAssignment.ts @@ -0,0 +1,86 @@ +import { + TSESTree, + AST_NODE_TYPES, +} from '@typescript-eslint/experimental-utils'; +import { isTypeReference } from 'tsutils'; +import * as ts from 'typescript'; +import { isTypeAnyType, isTypeUnknownType } from './predicates'; + +/** + * Does a simple check to see if there is an any being assigned to a non-any type. + * + * This also checks generic positions to ensure there's no unsafe sub-assignments. + * Note: in the case of generic positions, it makes the assumption that the two types are the same. + * + * @example See tests for examples + * + * @returns false if it's safe, or an object with the two types if it's unsafe + */ +export function isUnsafeAssignment( + type: ts.Type, + receiver: ts.Type, + checker: ts.TypeChecker, + senderNode: TSESTree.Node | null, +): false | { sender: ts.Type; receiver: ts.Type } { + if (isTypeAnyType(type)) { + // Allow assignment of any ==> unknown. + if (isTypeUnknownType(receiver)) { + return false; + } + + if (!isTypeAnyType(receiver)) { + return { sender: type, receiver }; + } + } + + if (isTypeReference(type) && isTypeReference(receiver)) { + // TODO - figure out how to handle cases like this, + // where the types are assignable, but not the same type + /* + function foo(): ReadonlySet { return new Set(); } + + // and + + type Test = { prop: T } + type Test2 = { prop: string } + declare const a: Test; + const b: Test2 = a; + */ + + if (type.target !== receiver.target) { + // if the type references are different, assume safe, as we won't know how to compare the two types + // the generic positions might not be equivalent for both types + return false; + } + + if ( + senderNode?.type === AST_NODE_TYPES.NewExpression && + senderNode.callee.type === AST_NODE_TYPES.Identifier && + senderNode.callee.name === 'Map' && + senderNode.arguments.length === 0 && + senderNode.typeParameters == null + ) { + // special case to handle `new Map()` + // unfortunately Map's default empty constructor is typed to return `Map` :( + // https://github.com/typescript-eslint/typescript-eslint/issues/2109#issuecomment-634144396 + return false; + } + + const typeArguments = type.typeArguments ?? []; + const receiverTypeArguments = receiver.typeArguments ?? []; + + for (let i = 0; i < typeArguments.length; i += 1) { + const arg = typeArguments[i]; + const receiverArg = receiverTypeArguments[i]; + + const unsafe = isUnsafeAssignment(arg, receiverArg, checker, senderNode); + if (unsafe) { + return { sender: type, receiver }; + } + } + + return false; + } + + return false; +} diff --git a/packages/type-utils/src/predicates.ts b/packages/type-utils/src/predicates.ts new file mode 100644 index 000000000000..16afbb25ea89 --- /dev/null +++ b/packages/type-utils/src/predicates.ts @@ -0,0 +1,152 @@ +import debug from 'debug'; +import { unionTypeParts } from 'tsutils'; +import * as ts from 'typescript'; +import { getTypeArguments } from './getTypeArguments'; +import { getTypeFlags, isTypeFlagSet } from './typeFlagUtils'; + +const log = debug('typescript-eslint:eslint-plugin:utils:types'); + +/** + * Checks if the given type is (or accepts) nullable + * @param isReceiver true if the type is a receiving type (i.e. the type of a called function's parameter) + */ +export function isNullableType( + type: ts.Type, + { + isReceiver = false, + allowUndefined = true, + }: { isReceiver?: boolean; allowUndefined?: boolean } = {}, +): boolean { + const flags = getTypeFlags(type); + + if (isReceiver && flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) { + return true; + } + + if (allowUndefined) { + return (flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) !== 0; + } else { + return (flags & ts.TypeFlags.Null) !== 0; + } +} + +/** + * Checks if the given type is either an array type, + * or a union made up solely of array types. + */ +export function isTypeArrayTypeOrUnionOfArrayTypes( + type: ts.Type, + checker: ts.TypeChecker, +): boolean { + for (const t of unionTypeParts(type)) { + if (!checker.isArrayType(t)) { + return false; + } + } + + return true; +} + +/** + * @returns true if the type is `unknown` + */ +export function isTypeUnknownType(type: ts.Type): boolean { + return isTypeFlagSet(type, ts.TypeFlags.Unknown); +} + +/** + * @returns true if the type is `any` + */ +export function isTypeAnyType(type: ts.Type): boolean { + if (isTypeFlagSet(type, ts.TypeFlags.Any)) { + if (type.intrinsicName === 'error') { + log('Found an "error" any type'); + } + return true; + } + return false; +} + +/** + * @returns true if the type is `any[]` + */ +export function isTypeAnyArrayType( + type: ts.Type, + checker: ts.TypeChecker, +): boolean { + return ( + checker.isArrayType(type) && + isTypeAnyType( + // getTypeArguments was only added in TS3.7 + getTypeArguments(type, checker)[0], + ) + ); +} + +/** + * @returns true if the type is `unknown[]` + */ +export function isTypeUnknownArrayType( + type: ts.Type, + checker: ts.TypeChecker, +): boolean { + return ( + checker.isArrayType(type) && + isTypeUnknownType( + // getTypeArguments was only added in TS3.7 + getTypeArguments(type, checker)[0], + ) + ); +} + +export enum AnyType { + Any, + AnyArray, + Safe, +} +/** + * @returns `AnyType.Any` if the type is `any`, `AnyType.AnyArray` if the type is `any[]` or `readonly any[]`, + * otherwise it returns `AnyType.Safe`. + */ +export function isAnyOrAnyArrayTypeDiscriminated( + node: ts.Node, + checker: ts.TypeChecker, +): AnyType { + const type = checker.getTypeAtLocation(node); + if (isTypeAnyType(type)) { + return AnyType.Any; + } + if (isTypeAnyArrayType(type, checker)) { + return AnyType.AnyArray; + } + return AnyType.Safe; +} + +/** + * @returns Whether a type is an instance of the parent type, including for the parent's base types. + */ +export function typeIsOrHasBaseType( + type: ts.Type, + parentType: ts.Type, +): boolean { + const parentSymbol = parentType.getSymbol(); + if (!type.getSymbol() || !parentSymbol) { + return false; + } + + const typeAndBaseTypes = [type]; + const ancestorTypes = type.getBaseTypes(); + + if (ancestorTypes) { + typeAndBaseTypes.push(...ancestorTypes); + } + + for (const baseType of typeAndBaseTypes) { + const baseSymbol = baseType.getSymbol(); + if (baseSymbol && baseSymbol.name === parentSymbol.name) { + return true; + } + } + + return false; +} diff --git a/packages/eslint-plugin/src/util/propertyTypes.ts b/packages/type-utils/src/propertyTypes.ts similarity index 100% rename from packages/eslint-plugin/src/util/propertyTypes.ts rename to packages/type-utils/src/propertyTypes.ts diff --git a/packages/eslint-plugin/src/util/requiresQuoting.ts b/packages/type-utils/src/requiresQuoting.ts similarity index 100% rename from packages/eslint-plugin/src/util/requiresQuoting.ts rename to packages/type-utils/src/requiresQuoting.ts diff --git a/packages/type-utils/src/typeFlagUtils.ts b/packages/type-utils/src/typeFlagUtils.ts new file mode 100644 index 000000000000..134fdcf4ece1 --- /dev/null +++ b/packages/type-utils/src/typeFlagUtils.ts @@ -0,0 +1,31 @@ +import { unionTypeParts } from 'tsutils'; +import * as ts from 'typescript'; + +/** + * Gets all of the type flags in a type, iterating through unions automatically + */ +export function getTypeFlags(type: ts.Type): ts.TypeFlags { + let flags: ts.TypeFlags = 0; + for (const t of unionTypeParts(type)) { + flags |= t.flags; + } + return flags; +} + +/** + * Checks if the given type is (or accepts) the given flags + * @param isReceiver true if the type is a receiving type (i.e. the type of a called function's parameter) + */ +export function isTypeFlagSet( + type: ts.Type, + flagsToCheck: ts.TypeFlags, + isReceiver?: boolean, +): boolean { + const flags = getTypeFlags(type); + + if (isReceiver && flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) { + return true; + } + + return (flags & flagsToCheck) !== 0; +} diff --git a/packages/type-utils/tests/fixtures/file.ts b/packages/type-utils/tests/fixtures/file.ts new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/type-utils/tests/fixtures/tsconfig.json b/packages/type-utils/tests/fixtures/tsconfig.json new file mode 100644 index 000000000000..65b63294fc1e --- /dev/null +++ b/packages/type-utils/tests/fixtures/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "jsx": "preserve", + "target": "es5", + "module": "commonjs", + "strict": true, + "esModuleInterop": true, + "lib": ["es2015", "es2017", "esnext"], + "experimentalDecorators": true + }, + "include": [ + "file.ts" + ] +} diff --git a/packages/eslint-plugin/tests/util/isUnsafeAssignment.test.ts b/packages/type-utils/tests/isUnsafeAssignment.test.ts similarity index 97% rename from packages/eslint-plugin/tests/util/isUnsafeAssignment.test.ts rename to packages/type-utils/tests/isUnsafeAssignment.test.ts index 1d4946a964de..060dc880e53a 100644 --- a/packages/eslint-plugin/tests/util/isUnsafeAssignment.test.ts +++ b/packages/type-utils/tests/isUnsafeAssignment.test.ts @@ -1,12 +1,11 @@ import * as ts from 'typescript'; import { TSESTree } from '@typescript-eslint/experimental-utils'; import { parseForESLint } from '@typescript-eslint/parser'; +import { isUnsafeAssignment } from '../src/isUnsafeAssignment'; import path from 'path'; -import { getFixturesRootDir } from '../RuleTester'; -import { isUnsafeAssignment } from '../../src/util/types'; describe('isUnsafeAssignment', () => { - const rootDir = getFixturesRootDir(); + const rootDir = path.join(__dirname, 'fixtures'); function getTypes(code: string): { sender: ts.Type; diff --git a/packages/type-utils/tsconfig.build.json b/packages/type-utils/tsconfig.build.json new file mode 100644 index 000000000000..3b5743aaf5d9 --- /dev/null +++ b/packages/type-utils/tsconfig.build.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./dist", + "rootDir": "./src", + "resolveJsonModule": true + }, + "include": ["src", "typings"], + "references": [{ "path": "../experimental-utils/tsconfig.build.json" }] +} diff --git a/packages/type-utils/tsconfig.json b/packages/type-utils/tsconfig.json new file mode 100644 index 000000000000..9c0c2ac4ba52 --- /dev/null +++ b/packages/type-utils/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.build.json", + "compilerOptions": { + "composite": false, + "rootDir": "." + }, + "include": ["src", "typings", "tests", "tools"], + "references": [{ "path": "../experimental-utils/tsconfig.build.json" }] +} diff --git a/packages/type-utils/typings/typescript.d.ts b/packages/type-utils/typings/typescript.d.ts new file mode 100644 index 000000000000..73304155ee74 --- /dev/null +++ b/packages/type-utils/typings/typescript.d.ts @@ -0,0 +1,33 @@ +import 'typescript'; + +declare module 'typescript' { + interface TypeChecker { + // internal TS APIs + + /** + * @returns `true` if the given type is an array type: + * - `Array` + * - `ReadonlyArray` + * - `foo[]` + * - `readonly foo[]` + */ + isArrayType(type: Type): type is TypeReference; + /** + * @returns `true` if the given type is a tuple type: + * - `[foo]` + * - `readonly [foo]` + */ + isTupleType(type: Type): type is TupleTypeReference; + /** + * Return the type of the given property in the given type, or undefined if no such property exists + */ + getTypeOfPropertyOfType(type: Type, propertyName: string): Type | undefined; + } + + interface Type { + /** + * If the type is `any`, and this is set to "error", then TS was unable to resolve the type + */ + intrinsicName?: string; + } +} diff --git a/packages/types/CHANGELOG.md b/packages/types/CHANGELOG.md index 872b4db197b2..35e056218337 100644 --- a/packages/types/CHANGELOG.md +++ b/packages/types/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [5.9.0](https://github.com/typescript-eslint/typescript-eslint/compare/v5.8.1...v5.9.0) (2022-01-03) + +**Note:** Version bump only for package @typescript-eslint/types + + + + + ## [5.8.1](https://github.com/typescript-eslint/typescript-eslint/compare/v5.8.0...v5.8.1) (2021-12-27) **Note:** Version bump only for package @typescript-eslint/types diff --git a/packages/types/package.json b/packages/types/package.json index 5c40bf8f4b14..02304e19a447 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/types", - "version": "5.8.1", + "version": "5.9.0", "description": "Types for the TypeScript-ESTree AST spec", "keywords": [ "eslint", diff --git a/packages/types/tools/copy-ast-spec.ts b/packages/types/tools/copy-ast-spec.ts index 0288453a3100..59bbf1e4b93f 100644 --- a/packages/types/tools/copy-ast-spec.ts +++ b/packages/types/tools/copy-ast-spec.ts @@ -56,4 +56,7 @@ async function main(): Promise { ]); } -void main(); +main().catch(error => { + console.error(error); + process.exitCode = 1; +}); diff --git a/packages/typescript-estree/CHANGELOG.md b/packages/typescript-estree/CHANGELOG.md index 9f5f9be7566b..aadf84718da3 100644 --- a/packages/typescript-estree/CHANGELOG.md +++ b/packages/typescript-estree/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [5.9.0](https://github.com/typescript-eslint/typescript-eslint/compare/v5.8.1...v5.9.0) (2022-01-03) + +**Note:** Version bump only for package @typescript-eslint/typescript-estree + + + + + ## [5.8.1](https://github.com/typescript-eslint/typescript-eslint/compare/v5.8.0...v5.8.1) (2021-12-27) **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 22f17a188c64..8a606a295801 100644 --- a/packages/typescript-estree/package.json +++ b/packages/typescript-estree/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/typescript-estree", - "version": "5.8.1", + "version": "5.9.0", "description": "A parser that converts TypeScript source code into an ESTree compatible form", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -41,8 +41,8 @@ "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { - "@typescript-eslint/types": "5.8.1", - "@typescript-eslint/visitor-keys": "5.8.1", + "@typescript-eslint/types": "5.9.0", + "@typescript-eslint/visitor-keys": "5.9.0", "debug": "^4.3.2", "globby": "^11.0.4", "is-glob": "^4.0.3", @@ -59,7 +59,7 @@ "@types/is-glob": "*", "@types/semver": "*", "@types/tmp": "*", - "@typescript-eslint/shared-fixtures": "5.8.1", + "@typescript-eslint/shared-fixtures": "5.9.0", "glob": "*", "jest-specific-snapshot": "*", "make-dir": "*", diff --git a/packages/typescript-estree/project.json b/packages/typescript-estree/project.json index a3e44b64373d..fe32123b5505 100644 --- a/packages/typescript-estree/project.json +++ b/packages/typescript-estree/project.json @@ -1,5 +1,5 @@ { "root": "packages/typescript-estree", "type": "library", - "implicitDependencies": [] + "implicitDependencies": ["@typescript-eslint/types"] } diff --git a/packages/visitor-keys/CHANGELOG.md b/packages/visitor-keys/CHANGELOG.md index 0c0dd43802c2..d5c597701d2c 100644 --- a/packages/visitor-keys/CHANGELOG.md +++ b/packages/visitor-keys/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [5.9.0](https://github.com/typescript-eslint/typescript-eslint/compare/v5.8.1...v5.9.0) (2022-01-03) + +**Note:** Version bump only for package @typescript-eslint/visitor-keys + + + + + ## [5.8.1](https://github.com/typescript-eslint/typescript-eslint/compare/v5.8.0...v5.8.1) (2021-12-27) **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 ef4d2344f82f..e31e97da895d 100644 --- a/packages/visitor-keys/package.json +++ b/packages/visitor-keys/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/visitor-keys", - "version": "5.8.1", + "version": "5.9.0", "description": "Visitor keys used to help traverse the TypeScript-ESTree AST", "keywords": [ "eslint", @@ -38,7 +38,7 @@ "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { - "@typescript-eslint/types": "5.8.1", + "@typescript-eslint/types": "5.9.0", "eslint-visitor-keys": "^3.0.0" }, "devDependencies": { diff --git a/packages/website-eslint/CHANGELOG.md b/packages/website-eslint/CHANGELOG.md index 29d30d757163..a6f31cb84e83 100644 --- a/packages/website-eslint/CHANGELOG.md +++ b/packages/website-eslint/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [5.9.0](https://github.com/typescript-eslint/typescript-eslint/compare/v5.8.1...v5.9.0) (2022-01-03) + +**Note:** Version bump only for package @typescript-eslint/website-eslint + + + + + ## [5.8.1](https://github.com/typescript-eslint/typescript-eslint/compare/v5.8.0...v5.8.1) (2021-12-27) **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 10a2949e90ab..059bad231f71 100644 --- a/packages/website-eslint/package.json +++ b/packages/website-eslint/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/website-eslint", - "version": "5.8.1", + "version": "5.9.0", "private": true, "description": "ESLint which works in browsers.", "engines": { @@ -16,19 +16,19 @@ "format": "prettier --write \"./**/*.{ts,js,json,md}\" --ignore-path ../../.prettierignore" }, "dependencies": { - "@typescript-eslint/experimental-utils": "5.8.1", - "@typescript-eslint/types": "5.8.1" + "@typescript-eslint/experimental-utils": "5.9.0", + "@typescript-eslint/types": "5.9.0" }, "devDependencies": { "@rollup/plugin-commonjs": "^21.0.1", "@rollup/plugin-json": "^4.1.0", "@rollup/plugin-node-resolve": "^13.0.6", "@rollup/pluginutils": "^4.1.1", - "@typescript-eslint/eslint-plugin": "5.8.1", - "@typescript-eslint/parser": "5.8.1", - "@typescript-eslint/scope-manager": "5.8.1", - "@typescript-eslint/typescript-estree": "5.8.1", - "@typescript-eslint/visitor-keys": "5.8.1", + "@typescript-eslint/eslint-plugin": "5.9.0", + "@typescript-eslint/parser": "5.9.0", + "@typescript-eslint/scope-manager": "5.9.0", + "@typescript-eslint/typescript-estree": "5.9.0", + "@typescript-eslint/visitor-keys": "5.9.0", "eslint": "*", "rollup": "^2.59.0", "semver": "^7.3.5" diff --git a/packages/website/CHANGELOG.md b/packages/website/CHANGELOG.md index ef2349b95d51..04972474c34d 100644 --- a/packages/website/CHANGELOG.md +++ b/packages/website/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [5.9.0](https://github.com/typescript-eslint/typescript-eslint/compare/v5.8.1...v5.9.0) (2022-01-03) + +**Note:** Version bump only for package website + + + + + ## [5.8.1](https://github.com/typescript-eslint/typescript-eslint/compare/v5.8.0...v5.8.1) (2021-12-27) **Note:** Version bump only for package website diff --git a/packages/website/package.json b/packages/website/package.json index a47a71afd09b..53d095b86005 100644 --- a/packages/website/package.json +++ b/packages/website/package.json @@ -1,6 +1,6 @@ { "name": "website", - "version": "5.8.1", + "version": "5.9.0", "private": true, "scripts": { "build": "docusaurus build", @@ -21,7 +21,7 @@ "@docusaurus/theme-classic": "^2.0.0-beta.13", "@docusaurus/theme-search-algolia": "^2.0.0-beta.13", "@mdx-js/react": "1.6.22", - "@typescript-eslint/website-eslint": "5.8.1", + "@typescript-eslint/website-eslint": "5.9.0", "clsx": "^1.1.1", "eslint": "*", "json5": "^2.2.0", diff --git a/packages/website/src/components/ASTViewerTS.tsx b/packages/website/src/components/ASTViewerTS.tsx index 6cd080262960..d33e524366aa 100644 --- a/packages/website/src/components/ASTViewerTS.tsx +++ b/packages/website/src/components/ASTViewerTS.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import ASTViewer from './ast/ASTViewer'; import type { ASTViewerBaseProps, ASTViewerModelMap } from './ast/types'; @@ -25,16 +25,6 @@ function extractEnum( return result; } -function getFlagNamesFromEnum( - allFlags: Record, - flags: number, - prefix: string, -): string[] { - return Object.entries(allFlags) - .filter(([f, _]) => (Number(f) & flags) !== 0) - .map(([_, name]) => `${prefix}.${name}`); -} - export default function ASTViewerTS({ value, position, @@ -45,54 +35,31 @@ export default function ASTViewerTS({ const [nodeFlags] = useState(() => extractEnum(window.ts.NodeFlags)); const [tokenFlags] = useState(() => extractEnum(window.ts.TokenFlags)); const [modifierFlags] = useState(() => extractEnum(window.ts.ModifierFlags)); + const [objectFlags] = useState(() => extractEnum(window.ts.ObjectFlags)); + const [symbolFlags] = useState(() => extractEnum(window.ts.SymbolFlags)); + const [flowFlags] = useState(() => extractEnum(window.ts.FlowFlags)); + const [typeFlags] = useState(() => extractEnum(window.ts.TypeFlags)); useEffect(() => { if (typeof value === 'string') { setModel(value); } else { - const scopeSerializer = createTsSerializer(value, syntaxKind); + const scopeSerializer = createTsSerializer( + value, + syntaxKind, + ['NodeFlags', nodeFlags], + ['TokenFlags', tokenFlags], + ['ModifierFlags', modifierFlags], + ['ObjectFlags', objectFlags], + ['SymbolFlags', symbolFlags], + ['FlowFlags', flowFlags], + ['TypeFlags', typeFlags], + ); setModel(serialize(value, scopeSerializer)); } }, [value, syntaxKind]); - // TODO: move this to serializer - const getTooltip = useCallback( - (data: ASTViewerModelMap): string | undefined => { - if (data.model.type === 'number') { - switch (data.key) { - case 'flags': - return getFlagNamesFromEnum( - nodeFlags, - Number(data.model.value), - 'NodeFlags', - ).join('\n'); - case 'numericLiteralFlags': - return getFlagNamesFromEnum( - tokenFlags, - Number(data.model.value), - 'TokenFlags', - ).join('\n'); - case 'modifierFlagsCache': - return getFlagNamesFromEnum( - modifierFlags, - Number(data.model.value), - 'ModifierFlags', - ).join('\n'); - case 'kind': - return `SyntaxKind.${syntaxKind[Number(data.model.value)]}`; - } - } - return undefined; - }, - [nodeFlags, tokenFlags, syntaxKind], - ); - return ( - + ); } diff --git a/packages/website/src/components/ast/ASTViewer.tsx b/packages/website/src/components/ast/ASTViewer.tsx index 8b54f3420d58..65edd757b7db 100644 --- a/packages/website/src/components/ast/ASTViewer.tsx +++ b/packages/website/src/components/ast/ASTViewer.tsx @@ -8,7 +8,6 @@ import { ElementItem } from './Elements'; function ASTViewer({ position, value, - getTooltip, onSelectNode, }: ASTViewerProps): JSX.Element { const [selection, setSelection] = useState(null); @@ -29,7 +28,6 @@ function ASTViewer({ ) : (
): JSX.Element { const [isExpanded, setIsExpanded] = useState(() => level === 'ast'); const [isSelected, setIsSelected] = useState(false); @@ -69,7 +68,6 @@ export function ComplexItem({ diff --git a/packages/website/src/components/ast/SimpleItem.tsx b/packages/website/src/components/ast/SimpleItem.tsx index cb20dc0ccbb8..c6e597dc8e9c 100644 --- a/packages/website/src/components/ast/SimpleItem.tsx +++ b/packages/website/src/components/ast/SimpleItem.tsx @@ -1,31 +1,19 @@ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback } from 'react'; import ItemGroup from './ItemGroup'; import Tooltip from '@site/src/components/inputs/Tooltip'; import PropertyValue from './PropertyValue'; -import type { - ASTViewerModelMapSimple, - GetTooltipFn, - OnSelectNodeFn, -} from './types'; +import type { ASTViewerModelMapSimple, OnSelectNodeFn } from './types'; export interface SimpleItemProps { - readonly getTooltip?: GetTooltipFn; readonly data: ASTViewerModelMapSimple; readonly onSelectNode?: OnSelectNodeFn; } export function SimpleItem({ - getTooltip, data, onSelectNode, }: SimpleItemProps): JSX.Element { - const [tooltip, setTooltip] = useState(); - - useEffect(() => { - setTooltip(getTooltip?.(data)); - }, [getTooltip, data]); - const onHover = useCallback( (state: boolean) => { if (onSelectNode && data.model.range) { @@ -37,8 +25,8 @@ export function SimpleItem({ return ( - {tooltip ? ( - + {data.model.tooltip ? ( + ) : ( diff --git a/packages/website/src/components/ast/serializer/serializer.ts b/packages/website/src/components/ast/serializer/serializer.ts index 3cbea9174a10..697dba624fb4 100644 --- a/packages/website/src/components/ast/serializer/serializer.ts +++ b/packages/website/src/components/ast/serializer/serializer.ts @@ -47,10 +47,20 @@ export function serialize( data: unknown, serializer?: Serializer, ): ASTViewerModelMap { - function processValue(data: [string, unknown][]): ASTViewerModelMap[] { - return data + function processValue( + data: [string, unknown][], + tooltip?: (data: ASTViewerModelMap) => string | undefined, + ): ASTViewerModelMap[] { + let result = data .filter(item => !item[0].startsWith('_') && item[1] !== undefined) .map(item => _serialize(item[1], item[0])); + if (tooltip) { + result = result.map(item => { + item.model.tooltip = tooltip(item); + return item; + }); + } + return result; } function _serialize(data: unknown, key?: string): ASTViewerModelMap { diff --git a/packages/website/src/components/ast/serializer/serializerTS.ts b/packages/website/src/components/ast/serializer/serializerTS.ts index e1a86f054a75..fd03d983ba76 100644 --- a/packages/website/src/components/ast/serializer/serializerTS.ts +++ b/packages/website/src/components/ast/serializer/serializerTS.ts @@ -1,5 +1,5 @@ import type { ASTViewerModel, Serializer, SelectedPosition } from '../types'; -import type { SourceFile, Node } from 'typescript'; +import type { SourceFile, Node, Type, Symbol as TSSymbol } from 'typescript'; import { isRecord } from '../utils'; export function getLineAndCharacterFor( @@ -15,7 +15,9 @@ export function getLineAndCharacterFor( export const propsToFilter = [ 'parent', + 'nextContainer', 'jsDoc', + 'jsDocComment', 'lineMap', 'externalModuleIndicator', 'bindDiagnostics', @@ -28,29 +30,120 @@ function isTsNode(value: unknown): value is Node { return isRecord(value) && typeof value.kind === 'number'; } +function isTsType(value: unknown): value is Type { + return isRecord(value) && value.getBaseTypes != null; +} + +function isTsSymbol(value: unknown): value is TSSymbol { + return isRecord(value) && value.getDeclarations != null; +} + +function expandFlags( + allFlags: [string, Record], + flags: number, +): string { + return Object.entries(allFlags[1]) + .filter(([f, _]) => (Number(f) & flags) !== 0) + .map(([_, name]) => `${allFlags[0]}.${name}`) + .join('\n'); +} + +function prepareValue(data: Record): [string, unknown][] { + return Object.entries(data).filter(item => !propsToFilter.includes(item[0])); +} + export function createTsSerializer( root: SourceFile, syntaxKind: Record, + nodeFlags: [string, Record], + tokenFlags: [string, Record], + modifierFlags: [string, Record], + objectFlags: [string, Record], + symbolFlags: [string, Record], + flowFlags: [string, Record], + typeFlags: [string, Record], ): Serializer { + const SEEN_THINGS = new WeakMap, ASTViewerModel>(); + return function serializer( data, - _key, + key, processValue, ): ASTViewerModel | undefined { - if (root && isTsNode(data)) { - const nodeName = syntaxKind[data.kind]; - - return { - range: { - start: getLineAndCharacterFor(data.pos, root), - end: getLineAndCharacterFor(data.end, root), - }, - type: 'object', - name: nodeName, - value: processValue( - Object.entries(data).filter(item => !propsToFilter.includes(item[0])), - ), - }; + if (root) { + if (isTsNode(data)) { + if (SEEN_THINGS.has(data)) { + return SEEN_THINGS.get(data); + } + + const nodeName = syntaxKind[data.kind]; + + const result: ASTViewerModel = { + range: { + start: getLineAndCharacterFor(data.pos, root), + end: getLineAndCharacterFor(data.end, root), + }, + type: 'object', + name: nodeName, + value: [], + }; + + SEEN_THINGS.set(data, result); + + result.value = processValue(prepareValue(data), item => { + if (item.model.type === 'number') { + switch (item.key) { + case 'flags': + return expandFlags(nodeFlags, Number(item.model.value)); + case 'numericLiteralFlags': + return expandFlags(tokenFlags, Number(item.model.value)); + case 'modifierFlagsCache': + return expandFlags(modifierFlags, Number(item.model.value)); + case 'kind': + return `SyntaxKind.${syntaxKind[Number(item.model.value)]}`; + } + } + return undefined; + }); + return result; + } else if (isTsType(data)) { + return { + type: 'object', + name: '[Type]', + value: processValue(prepareValue(data), item => { + if (item.model.type === 'number') { + if (item.key === 'objectFlags') { + return expandFlags(objectFlags, Number(item.model.value)); + } else if (item.key === 'flags') { + return expandFlags(typeFlags, Number(item.model.value)); + } + } + return undefined; + }), + }; + } else if (isTsSymbol(data)) { + return { + type: 'object', + name: '[Symbol]', + value: processValue(prepareValue(data), item => { + if (item.model.type === 'number' && item.key === 'flags') { + return expandFlags(symbolFlags, Number(item.model.value)); + } + return undefined; + }), + }; + } else if (key === 'flowNode' || key === 'endFlowNode') { + return { + type: 'object', + name: '[FlowNode]', + value: processValue(prepareValue(data), item => { + if (item.model.type === 'number' && item.key === 'flags') { + return expandFlags(flowFlags, Number(item.model.value)); + } + return undefined; + }), + }; + } } return undefined; }; diff --git a/packages/website/src/components/ast/types.ts b/packages/website/src/components/ast/types.ts index 1d5976424d2d..76f5164d07b6 100644 --- a/packages/website/src/components/ast/types.ts +++ b/packages/website/src/components/ast/types.ts @@ -1,7 +1,6 @@ import type { SelectedPosition, SelectedRange } from '../types'; import Monaco from 'monaco-editor'; -export type GetTooltipFn = (data: ASTViewerModelMap) => string | undefined; export type OnSelectNodeFn = (node: SelectedRange | null) => void; export type ASTViewerModelTypeSimple = @@ -19,6 +18,7 @@ export type ASTViewerModelTypeComplex = 'object' | 'array'; export interface ASTViewerModelBase { name?: string; range?: SelectedRange; + tooltip?: string; } export interface ASTViewerModelSimple extends ASTViewerModelBase { @@ -46,7 +46,6 @@ export interface GenericParams { readonly level: string; readonly selection?: SelectedPosition | null; readonly onSelectNode?: OnSelectNodeFn; - readonly getTooltip?: GetTooltipFn; } export interface ASTViewerBaseProps { @@ -55,14 +54,16 @@ export interface ASTViewerBaseProps { } export interface ASTViewerProps extends ASTViewerBaseProps { - readonly getTooltip?: GetTooltipFn; readonly value: ASTViewerModelMap | string; } export type Serializer = ( data: Record, key: string | undefined, - processValue: (data: [string, unknown][]) => ASTViewerModelMap[], + processValue: ( + data: [string, unknown][], + tooltip?: (data: ASTViewerModelMap) => string | undefined, + ) => ASTViewerModelMap[], ) => ASTViewerModel | undefined; export type { SelectedPosition, SelectedRange }; diff --git a/tests/integration/integration-test-base.ts b/tests/integration/integration-test-base.ts index 71c5934d9454..f49a4e90a232 100644 --- a/tests/integration/integration-test-base.ts +++ b/tests/integration/integration-test-base.ts @@ -62,6 +62,11 @@ export function integrationTest(testFilename: string, filesGlob: string): void { ? rootPackageJson.devDependencies.tslint : undefined, }, + // ensure everything uses the locally packed versions instead of the NPM versions + resolutions: { + // @ts-expect-error -- this is in `./pack-packages.ts` + ...global.tseslintPackages, + }, }), ); // console.log('package.json written.'); diff --git a/workspace.json b/workspace.json index f6b206e61edb..6fbf76fb0786 100644 --- a/workspace.json +++ b/workspace.json @@ -10,6 +10,7 @@ "@typescript-eslint/scope-manager": "packages/scope-manager", "@typescript-eslint/shared-fixtures": "packages/shared-fixtures", "@typescript-eslint/types": "packages/types", + "@typescript-eslint/type-utils": "packages/type-utils", "@typescript-eslint/typescript-estree": "packages/typescript-estree", "@typescript-eslint/visitor-keys": "packages/visitor-keys", "@typescript-eslint/website-eslint": "packages/website-eslint", diff --git a/yarn.lock b/yarn.lock index 6e6be599b811..3c305e231a21 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1187,10 +1187,10 @@ resolve-global "1.0.0" yargs "^17.0.0" -"@commitlint/config-conventional@^15.0.0": - version "15.0.0" - resolved "https://registry.yarnpkg.com/@commitlint/config-conventional/-/config-conventional-15.0.0.tgz#3bf1adf319e3b431de12ba82dc399524038b2d8f" - integrity sha512-eZBRL8Lk3hMNHp1wUMYj0qrZQEsST1ai7KHR8J1IDD9aHgT7L2giciibuQ+Og7vxVhR5WtYDvh9xirXFVPaSkQ== +"@commitlint/config-conventional@^16.0.0": + version "16.0.0" + resolved "https://registry.yarnpkg.com/@commitlint/config-conventional/-/config-conventional-16.0.0.tgz#f42d9e1959416b5e691c8b5248fc2402adb1fc03" + integrity sha512-mN7J8KlKFn0kROd+q9PB01sfDx/8K/R25yITspL1No8PB4oj9M1p77xWjP80hPydqZG9OvQq+anXK3ZWeR7s3g== dependencies: conventional-changelog-conventionalcommits "^4.3.1" @@ -3782,9 +3782,9 @@ integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ== "@types/react-helmet@*", "@types/react-helmet@^6.1.4": - version "6.1.4" - resolved "https://registry.yarnpkg.com/@types/react-helmet/-/react-helmet-6.1.4.tgz#3e54a3eb37ba7fb34ffafc64f425be4e68df03b9" - integrity sha512-jyx50RNZXVaTGHY3MsoRPNpeiVk8b0XTPgD/O6KHF6COTDnG/+lRjPYvTK5nfWtR3xDOux0w6bHLAsaHo2ZLTA== + version "6.1.5" + resolved "https://registry.yarnpkg.com/@types/react-helmet/-/react-helmet-6.1.5.tgz#35f89a6b1646ee2bc342a33a9a6c8777933f9083" + integrity sha512-/ICuy7OHZxR0YCAZLNg9r7I9aijWUWvxaPR6uTuyxe8tAj5RL4Sw1+R6NhXUtOsarkGYPmaHdBDvuXh2DIN/uA== dependencies: "@types/react" "*" @@ -3815,9 +3815,9 @@ "@types/react" "*" "@types/react@*", "@types/react@^17.0.34": - version "17.0.37" - resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.37.tgz#6884d0aa402605935c397ae689deed115caad959" - integrity sha512-2FS1oTqBGcH/s0E+CjrCCR9+JMpsu9b69RTFO+40ua43ZqP5MmQ4iUde/dMjWR909KxZwmOQIFq6AV6NjEG5xg== + version "17.0.38" + resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.38.tgz#f24249fefd89357d5fa71f739a686b8d7c7202bd" + integrity sha512-SI92X1IA+FMnP3qM5m4QReluXzhcmovhZnLNm3pyeQlooi02qI7sLiepEYqT678uNiyc25XfCqxREFpy3W7YhQ== dependencies: "@types/prop-types" "*" "@types/scheduler" "*" @@ -6652,9 +6652,9 @@ eslint-plugin-react-hooks@^4.3.0: integrity sha512-XslZy0LnMn+84NEG9jSGR6eGqaZB3133L8xewQo3fQagbQuGt7a63gf+P1NGKZavEYEC3UXaWEAA/AqDkuN6xA== eslint-plugin-react@^7.27.1: - version "7.27.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.27.1.tgz#469202442506616f77a854d91babaae1ec174b45" - integrity sha512-meyunDjMMYeWr/4EBLTV1op3iSG3mjT/pz5gti38UzfM4OPpNc2m0t2xvKCOMU5D6FSdd34BIMFOvQbW+i8GAA== + version "7.28.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.28.0.tgz#8f3ff450677571a659ce76efc6d80b6a525adbdf" + integrity sha512-IOlFIRHzWfEQQKcAD4iyYDndHwTQiCMcJVJjxempf203jnNLUnW34AXLrV33+nEXoifJE2ZEGmcjKPL8957eSw== dependencies: array-includes "^3.1.4" array.prototype.flatmap "^1.2.5" @@ -9597,9 +9597,9 @@ linkify-it@^3.0.1: uc.micro "^1.0.1" lint-staged@^12.0.2: - version "12.1.3" - resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-12.1.3.tgz#a16e885c0a5e77de9cf559724d29a10348670e68" - integrity sha512-ajapdkaFxx+MVhvq6xQRg9zCnCLz49iQLJZP7+w8XaA3U4B35Z9xJJGq9vxmWo73QTvJLG+N2NxhjWiSexbAWQ== + version "12.1.4" + resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-12.1.4.tgz#a92ec8509f13018caaafade61d515c2d5873316e" + integrity sha512-RgDz9nsFsE0/5eL9Vat0AvCuk0+j5mEuzBIVfrRH5FRtt5wibYe8zTjZs2nuqLFrLAGQGYnj8+HJxolcj08i/A== dependencies: cli-truncate "^3.1.0" colorette "^2.0.16"