diff --git a/.cspell.json b/.cspell.json index 39e8c795e7af..0c045aaa4bc7 100644 --- a/.cspell.json +++ b/.cspell.json @@ -9,7 +9,8 @@ "**/**/CHANGELOG.md", "**/**/CONTRIBUTORS.md", "**/**/ROADMAP.md", - "**/*.{json,snap}" + "**/*.{json,snap}", + ".cspell.json" ], "dictionaries": [ "typescript", @@ -54,6 +55,8 @@ "destructure", "destructured", "erroring", + "ESLint", + "ESLint's", "espree", "estree", "linebreaks", diff --git a/CHANGELOG.md b/CHANGELOG.md index a36543570994..3b933e712e88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,31 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.16.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.15.0...v2.16.0) (2020-01-13) + + +### Bug Fixes + +* **eslint-plugin:** [no-magic-numbers] handle UnaryExpression for enums ([#1415](https://github.com/typescript-eslint/typescript-eslint/issues/1415)) ([852fc31](https://github.com/typescript-eslint/typescript-eslint/commit/852fc31)) +* **eslint-plugin:** [no-unnec-type-assert] handle JSX attributes ([#1002](https://github.com/typescript-eslint/typescript-eslint/issues/1002)) ([3c5659b](https://github.com/typescript-eslint/typescript-eslint/commit/3c5659b)) +* **eslint-plugin:** handle error classes using generics ([#1428](https://github.com/typescript-eslint/typescript-eslint/issues/1428)) ([b139540](https://github.com/typescript-eslint/typescript-eslint/commit/b139540)) +* **typescript-estree:** fix persisted parse for relative paths ([#1424](https://github.com/typescript-eslint/typescript-eslint/issues/1424)) ([9720d2c](https://github.com/typescript-eslint/typescript-eslint/commit/9720d2c)) +* **typescript-estree:** parsing of deeply nested new files in new folder ([#1412](https://github.com/typescript-eslint/typescript-eslint/issues/1412)) ([206c94b](https://github.com/typescript-eslint/typescript-eslint/commit/206c94b)) +* **typescript-estree:** resolve path relative to tsconfigRootDir ([#1439](https://github.com/typescript-eslint/typescript-eslint/issues/1439)) ([c709056](https://github.com/typescript-eslint/typescript-eslint/commit/c709056)) + + +### Features + +* **eslint-plugin:** [no-unnec-cond] array predicate callbacks ([#1206](https://github.com/typescript-eslint/typescript-eslint/issues/1206)) ([f7ad716](https://github.com/typescript-eslint/typescript-eslint/commit/f7ad716)) +* **eslint-plugin:** add default-param-last rule ([#1418](https://github.com/typescript-eslint/typescript-eslint/issues/1418)) ([a37ff9f](https://github.com/typescript-eslint/typescript-eslint/commit/a37ff9f)) +* **eslint-plugin:** add rule naming-conventions ([#1318](https://github.com/typescript-eslint/typescript-eslint/issues/1318)) ([9eab26f](https://github.com/typescript-eslint/typescript-eslint/commit/9eab26f)) +* **typescript-estree:** add parserOption to turn on debug logs ([#1413](https://github.com/typescript-eslint/typescript-eslint/issues/1413)) ([25092fd](https://github.com/typescript-eslint/typescript-eslint/commit/25092fd)) +* **typescript-estree:** add strict type mapping to esTreeNodeToTSNodeMap ([#1382](https://github.com/typescript-eslint/typescript-eslint/issues/1382)) ([d3d70a3](https://github.com/typescript-eslint/typescript-eslint/commit/d3d70a3)) + + + + + # [2.15.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.14.0...v2.15.0) (2020-01-06) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 4b20b6a825be..8eca71b7c172 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -31,18 +31,10 @@ jobs: yarn lint displayName: 'Run linting' - - script: | - yarn check:docs - displayName: 'Validate documentation' - - script: | yarn check:spelling displayName: 'Validate documentation spelling' - - script: | - yarn check:configs - displayName: 'Validate plugin configs' - - script: | yarn test displayName: 'Run unit tests' diff --git a/lerna.json b/lerna.json index 1363f6a2904d..4e1f468786f5 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.15.0", + "version": "2.16.0", "npmClient": "yarn", "useWorkspaces": true, "stream": true diff --git a/package.json b/package.json index 8255719940d2..76fed22655ac 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "cz": "git-cz", "check:docs": "lerna run check:docs", "check:configs": "lerna run check:configs", - "check:spelling": "cspell --config=.cspell.json **/*.{md,ts,js}", + "check:spelling": "cspell --config=.cspell.json \"**/*.{md,ts,js}\"", "generate-contributors": "yarn ts-node --transpile-only ./tools/generate-contributors.ts && yarn all-contributors generate", "format": "prettier --write \"./**/*.{ts,js,json,md}\"", "format-check": "prettier --list-different \"./**/*.{ts,js,json,md}\"", diff --git a/packages/eslint-plugin-internal/CHANGELOG.md b/packages/eslint-plugin-internal/CHANGELOG.md index 4d6e597d1ffb..0ea39452d95f 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. +# [2.16.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.15.0...v2.16.0) (2020-01-13) + +**Note:** Version bump only for package @typescript-eslint/eslint-plugin-internal + + + + + # [2.15.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.14.0...v2.15.0) (2020-01-06) **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 9d35aef53a87..603b3f244e35 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": "2.15.0", + "version": "2.16.0", "private": true, "main": "dist/index.js", "scripts": { @@ -12,6 +12,6 @@ "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { - "@typescript-eslint/experimental-utils": "2.15.0" + "@typescript-eslint/experimental-utils": "2.16.0" } } diff --git a/packages/eslint-plugin-tslint/CHANGELOG.md b/packages/eslint-plugin-tslint/CHANGELOG.md index e7ed64869469..42e6d600e4fb 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. +# [2.16.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.15.0...v2.16.0) (2020-01-13) + +**Note:** Version bump only for package @typescript-eslint/eslint-plugin-tslint + + + + + # [2.15.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.14.0...v2.15.0) (2020-01-06) **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 b85ae6a5d558..c9933a31e8c1 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": "2.15.0", + "version": "2.16.0", "main": "dist/index.js", "typings": "src/index.ts", "description": "TSLint wrapper plugin for ESLint", @@ -31,8 +31,8 @@ "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { - "@typescript-eslint/experimental-utils": "2.15.0", - "lodash.memoize": "^4.1.2" + "@typescript-eslint/experimental-utils": "2.16.0", + "lodash": "^4.17.15" }, "peerDependencies": { "eslint": "^5.0.0 || ^6.0.0", @@ -40,7 +40,7 @@ "typescript": "*" }, "devDependencies": { - "@types/lodash.memoize": "^4.1.4", - "@typescript-eslint/parser": "2.15.0" + "@types/lodash": "^4.14.149", + "@typescript-eslint/parser": "2.16.0" } } diff --git a/packages/eslint-plugin-tslint/src/rules/config.ts b/packages/eslint-plugin-tslint/src/rules/config.ts index 3d439366a89b..05608387e79a 100644 --- a/packages/eslint-plugin-tslint/src/rules/config.ts +++ b/packages/eslint-plugin-tslint/src/rules/config.ts @@ -2,7 +2,7 @@ import { ESLintUtils, ParserServices, } from '@typescript-eslint/experimental-utils'; -import memoize from 'lodash.memoize'; +import memoize from 'lodash/memoize'; import { Configuration, RuleSeverity } from 'tslint'; import { CustomLinter } from '../custom-linter'; diff --git a/packages/eslint-plugin/CHANGELOG.md b/packages/eslint-plugin/CHANGELOG.md index 3ecaf883e0e9..ea96304a87d3 100644 --- a/packages/eslint-plugin/CHANGELOG.md +++ b/packages/eslint-plugin/CHANGELOG.md @@ -3,6 +3,28 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.16.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.15.0...v2.16.0) (2020-01-13) + + +### Bug Fixes + +* **eslint-plugin:** [no-magic-numbers] handle UnaryExpression for enums ([#1415](https://github.com/typescript-eslint/typescript-eslint/issues/1415)) ([852fc31](https://github.com/typescript-eslint/typescript-eslint/commit/852fc31)) +* **eslint-plugin:** [no-unnec-type-assert] handle JSX attributes ([#1002](https://github.com/typescript-eslint/typescript-eslint/issues/1002)) ([3c5659b](https://github.com/typescript-eslint/typescript-eslint/commit/3c5659b)) +* **eslint-plugin:** handle error classes using generics ([#1428](https://github.com/typescript-eslint/typescript-eslint/issues/1428)) ([b139540](https://github.com/typescript-eslint/typescript-eslint/commit/b139540)) +* **typescript-estree:** resolve path relative to tsconfigRootDir ([#1439](https://github.com/typescript-eslint/typescript-eslint/issues/1439)) ([c709056](https://github.com/typescript-eslint/typescript-eslint/commit/c709056)) + + +### Features + +* **eslint-plugin:** [no-unnec-cond] array predicate callbacks ([#1206](https://github.com/typescript-eslint/typescript-eslint/issues/1206)) ([f7ad716](https://github.com/typescript-eslint/typescript-eslint/commit/f7ad716)) +* **eslint-plugin:** add default-param-last rule ([#1418](https://github.com/typescript-eslint/typescript-eslint/issues/1418)) ([a37ff9f](https://github.com/typescript-eslint/typescript-eslint/commit/a37ff9f)) +* **eslint-plugin:** add rule naming-conventions ([#1318](https://github.com/typescript-eslint/typescript-eslint/issues/1318)) ([9eab26f](https://github.com/typescript-eslint/typescript-eslint/commit/9eab26f)) +* **typescript-estree:** add strict type mapping to esTreeNodeToTSNodeMap ([#1382](https://github.com/typescript-eslint/typescript-eslint/issues/1382)) ([d3d70a3](https://github.com/typescript-eslint/typescript-eslint/commit/d3d70a3)) + + + + + # [2.15.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.14.0...v2.15.0) (2020-01-06) diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index a332bafec80c..2e4c9c5ae46f 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -101,19 +101,16 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int | [`@typescript-eslint/ban-ts-ignore`](./docs/rules/ban-ts-ignore.md) | Bans “// @ts-ignore” comments from being used | :heavy_check_mark: | | | | [`@typescript-eslint/ban-types`](./docs/rules/ban-types.md) | Bans specific types from being used | :heavy_check_mark: | :wrench: | | | [`@typescript-eslint/brace-style`](./docs/rules/brace-style.md) | Enforce consistent brace style for blocks | | :wrench: | | -| [`@typescript-eslint/camelcase`](./docs/rules/camelcase.md) | Enforce camelCase naming convention | :heavy_check_mark: | | | -| [`@typescript-eslint/class-name-casing`](./docs/rules/class-name-casing.md) | Require PascalCased class and interface names | :heavy_check_mark: | | | | [`@typescript-eslint/consistent-type-assertions`](./docs/rules/consistent-type-assertions.md) | Enforces consistent usage of type assertions | :heavy_check_mark: | | | | [`@typescript-eslint/consistent-type-definitions`](./docs/rules/consistent-type-definitions.md) | Consistent with type definition either `interface` or `type` | | :wrench: | | +| [`@typescript-eslint/default-param-last`](./docs/rules/default-param-last.md) | Enforce default parameters to be last | | | | | [`@typescript-eslint/explicit-function-return-type`](./docs/rules/explicit-function-return-type.md) | Require explicit return types on functions and class methods | :heavy_check_mark: | | | | [`@typescript-eslint/explicit-member-accessibility`](./docs/rules/explicit-member-accessibility.md) | Require explicit accessibility modifiers on class properties and methods | | | | | [`@typescript-eslint/func-call-spacing`](./docs/rules/func-call-spacing.md) | Require or disallow spacing between function identifiers and their invocations | | :wrench: | | -| [`@typescript-eslint/generic-type-naming`](./docs/rules/generic-type-naming.md) | Enforces naming of generic type variables | | | | | [`@typescript-eslint/indent`](./docs/rules/indent.md) | Enforce consistent indentation | | :wrench: | | -| [`@typescript-eslint/interface-name-prefix`](./docs/rules/interface-name-prefix.md) | Require that interface names should or should not prefixed with `I` | :heavy_check_mark: | | | | [`@typescript-eslint/member-delimiter-style`](./docs/rules/member-delimiter-style.md) | Require a specific member delimiter style for interfaces and type literals | :heavy_check_mark: | :wrench: | | -| [`@typescript-eslint/member-naming`](./docs/rules/member-naming.md) | Enforces naming conventions for class members by visibility | | | | | [`@typescript-eslint/member-ordering`](./docs/rules/member-ordering.md) | Require a consistent member declaration order | | | | +| [`@typescript-eslint/naming-convention`](./docs/rules/naming-convention.md) | Enforces naming conventions for everything across a codebase | | | :thought_balloon: | | [`@typescript-eslint/no-array-constructor`](./docs/rules/no-array-constructor.md) | Disallow generic `Array` constructors | :heavy_check_mark: | :wrench: | | | [`@typescript-eslint/no-dynamic-delete`](./docs/rules/no-dynamic-delete.md) | Disallow the delete operator with computed key expressions | | :wrench: | | | [`@typescript-eslint/no-empty-function`](./docs/rules/no-empty-function.md) | Disallow empty functions | :heavy_check_mark: | | | @@ -159,7 +156,7 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int | [`@typescript-eslint/prefer-string-starts-ends-with`](./docs/rules/prefer-string-starts-ends-with.md) | Enforce the use of `String#startsWith` and `String#endsWith` instead of other equivalent methods of checking substrings | :heavy_check_mark: | :wrench: | :thought_balloon: | | [`@typescript-eslint/promise-function-async`](./docs/rules/promise-function-async.md) | Requires any function or method that returns a Promise to be marked async | | | :thought_balloon: | | [`@typescript-eslint/quotes`](./docs/rules/quotes.md) | Enforce the consistent use of either backticks, double, or single quotes | | :wrench: | | -| [`@typescript-eslint/require-array-sort-compare`](./docs/rules/require-array-sort-compare.md) | Enforce giving `compare` argument to `Array#sort` | | | :thought_balloon: | +| [`@typescript-eslint/require-array-sort-compare`](./docs/rules/require-array-sort-compare.md) | Requires `Array#sort` calls to always provide a `compareFunction` | | | :thought_balloon: | | [`@typescript-eslint/require-await`](./docs/rules/require-await.md) | Disallow async functions which have no `await` expression | :heavy_check_mark: | | :thought_balloon: | | [`@typescript-eslint/restrict-plus-operands`](./docs/rules/restrict-plus-operands.md) | When adding two variables, operands must both be of type number or of type string | | | :thought_balloon: | | [`@typescript-eslint/restrict-template-expressions`](./docs/rules/restrict-template-expressions.md) | Enforce template literal expressions to be of string type | | | :thought_balloon: | diff --git a/packages/eslint-plugin/ROADMAP.md b/packages/eslint-plugin/ROADMAP.md index bca59dc84cff..20661b22ea0b 100644 --- a/packages/eslint-plugin/ROADMAP.md +++ b/packages/eslint-plugin/ROADMAP.md @@ -1,6 +1,6 @@ # TSLint Migration Guide -This document serves as a guid to help you migrate from TSLint. +This document serves as a guide to help you migrate from TSLint. It lists all TSLint rules along side rules from the ESLint ecosystem that are the same or similar. ## TSLint rules diff --git a/packages/eslint-plugin/docs/rules/default-param-last.md b/packages/eslint-plugin/docs/rules/default-param-last.md new file mode 100644 index 000000000000..8f2518c50ae5 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/default-param-last.md @@ -0,0 +1,29 @@ +# Enforce default parameters to be last (`default-param-last`) + +## Rule Details + +This rule enforces default or optional parameters to be the last of parameters. + +Examples of **incorrect** code for this rule: + +```ts +/* eslint @typescript-eslint/default-param-last: ["error"] */ + +function f(a = 0, b: number) {} +function f(a: number, b = 0, c?: number) {} +function f(a: number, b = 0, c: number) {} +function f(a: number, b?: number, c: number) {} +``` + +Examples of **correct** code for this rule: + +```ts +/* eslint @typescript-eslint/default-param-last: ["error"] */ + +function f(a = 0) {} +function f(a: number, b = 0) {} +function f(a: number, b?: number) {} +function f(a: number, b?: number, c = 0) {} +``` + +Taken with ❤️ [from ESLint core](https://github.com/eslint/eslint/blob/master/docs/rules/default-param-last.md) diff --git a/packages/eslint-plugin/docs/rules/explicit-member-accessibility.md b/packages/eslint-plugin/docs/rules/explicit-member-accessibility.md index 3a10696fa11e..9b148d95c85f 100644 --- a/packages/eslint-plugin/docs/rules/explicit-member-accessibility.md +++ b/packages/eslint-plugin/docs/rules/explicit-member-accessibility.md @@ -59,8 +59,7 @@ If you are working on a codebase within which you lint non-TypeScript code (i.e. ### `accessibility` -This rule in it's default state requires no configuration and will enforce that every class member has an accessibility modifier. If you would like to allow for some implicit public members then you have the following options: -A possible configuration could be: +This rule in its default state requires no configuration and will enforce that every class member has an accessibility modifier. If you would like to allow for some implicit public members then you have the following options: ```ts { @@ -75,6 +74,8 @@ A possible configuration could be: } ``` +Note the above is an example of a possible configuration you could use - it is not the default configuration. + The following patterns are considered incorrect code if no options are provided: ```ts diff --git a/packages/eslint-plugin/docs/rules/naming-convention.md b/packages/eslint-plugin/docs/rules/naming-convention.md new file mode 100644 index 000000000000..5186c98eb6db --- /dev/null +++ b/packages/eslint-plugin/docs/rules/naming-convention.md @@ -0,0 +1,338 @@ +# Enforces naming conventions for everything across a codebase (`naming-convention`) + +Enforcing naming conventions helps keep the codebase consistent, and reduces overhead when thinking about how to name a variable. +Additionally, a well designed style guide can help communicate intent, such as by enforcing all private properties begin with an `_`, and all global-level constants are written in `UPPER_CASE`. + +There are many different rules that have existed over time, but they have had the problem of not having enough granularity, meaning it was hard to have a well defined style guide, and most of the time you needed 3 or more rules at once to enforce different conventions, hoping they didn't conflict. + +## Rule Details + +This rule allows you to enforce conventions for any identifier, using granular selectors to create a fine-grained style guide. + +### Note - this rule only needs type information in specific cases, detailed below + +## Options + +This rule accepts an array of objects, with each object describing a different naming convention. +Each property will be described in detail below. Also see the examples section below for illustrated examples. + +```ts +type Options = { + // format options + format: ( + | 'camelCase' + | 'strictCamelCase' + | 'PascalCase' + | 'StrictPascalCase' + | 'snake_case' + | 'UPPER_CASE' + )[]; + custom?: { + regex: string; + match: boolean; + }; + leadingUnderscore?: 'forbid' | 'allow' | 'require'; + trailingUnderscore?: 'forbid' | 'allow' | 'require'; + prefix?: string[]; + suffix?: string[]; + + // selector options + selector: Selector; + filter?: string; + // the allowed values for these are dependent on the selector - see below + modifiers?: Modifiers[]; + types?: Types[]; +}[]; + +// the default config essentially does the same thing as ESLint's camelcase rule +const defaultOptions: Options = [ + { + selector: 'default', + format: ['camelCase'], + leadingUnderscore: 'allow', + trailingUnderscore: 'allow', + }, + + { + selector: 'variable', + format: ['camelCase', 'UPPER_CASE'], + leadingUnderscore: 'allow', + trailingUnderscore: 'allow', + }, + + { + selector: 'typeLike', + format: ['PascalCase'], + }, +]; +``` + +### Format Options + +Every single selector can have the same set of format options. +When the format of an identifier is checked, it is checked in the following order: + +1. validate leading underscore +1. validate trailing underscore +1. validate prefix +1. validate suffix +1. validate custom +1. validate format + +At each step, if the identifier matches the option, the matching part will be removed. +For example, if you provide the following formatting option: `{ leadingUnderscore: 'allow', prefix: ['I'], format: ['StrictPascalCase'] }`, for the identifier `_IMyInterface`, then the following checks will occur: + +1. `name = _IMyInterface` +1. validate leading underscore - pass + - Trim leading underscore - `name = IMyInterface` +1. validate trailing underscore - no check +1. validate prefix - pass + - Trim prefix - `name = MyInterface` +1. validate suffix - no check +1. validate format - pass + +One final note is that if the name were to become empty via this trimming process, it is considered to match all `format`s. An example of where this might be useful is for generic type parameters, where you want all names to be prefixed with `T`, but also want to allow for the single character `T` name. + +#### `format` + +The `format` option defines the allowed formats for the identifier. This option accepts an array of the following values, and the identifier can match any of them: + +- `camelCase` - standard camelCase format - no underscores are allowed between characters, and consecutive capitals are allowed (i.e. both `myID` and `myId` are valid). +- `strictCamelCase` - same as `camelCase`, but consecutive capitals are not allowed (i.e. `myId` is valid, but `myID` is not). +- `PascalCase` - same as `camelCase`, except the first character must be upper-case. +- `StrictPascalCase` - same as `strictCamelCase`, except the first character must be upper-case. +- `snake_case` - standard snake_case format - all characters must be lower-case, and underscores are allowed. +- `UPPER_CASE` - same as `snake_case`, except all characters must be upper-case. + +### `custom` + +The `custom` option defines a custom regex that the identifier must (or must not) match. This option allows you to have a bit more finer-grained control over identifiers, letting you ban (or force) certain patterns and substrings. +Accepts an object with the following properties: + +- `regex` - accepts a regular expression (anything accepted into `new RegExp(filter)`). +- `match` - true if the identifier _must_ match the `regex`, false if the identifier _must not_ match the `regex`. + +#### `leadingUnderscore` / `trailingUnderscore` + +The `leadingUnderscore` / `trailingUnderscore` options control whether leading/trailing underscores are considered valid. Accepts one of the following values: + +- `forbid` - a leading/trailing underscore is not allowed at all. +- `allow` - existence of a leading/trailing underscore is not explicitly enforced. +- `require` - a leading/trailing underscore must be included. + +#### `prefix` / `suffix` + +The `prefix` / `suffix` options control which prefix/suffix strings must exist for the identifier. Accepts an array of strings. + +If these are provided, the identifier must start with one of the provided values. For example, if you provide `{ prefix: ['IFace', 'Class', 'Type'] }`, then the following names are valid: `IFaceFoo`, `ClassBar`, `TypeBaz`, but the name `Bang` is not valid, as it contains none of the prefixes. + +### Selector Options + +- `selector` (see "Allowed Selectors, Modifiers and Types" below). +- `filter` accepts a regular expression (anything accepted into `new RegExp(filter)`). It allows you to limit the scope of this configuration to names that match this regex. +- `modifiers` allows you to specify which modifiers to granularly apply to, such as the accessibility (`private`/`public`/`protected`), or if the thing is `static`, etc. + - The name must match _all_ of the modifiers. + - For example, if you provide `{ modifiers: ['private', 'static', 'readonly'] }`, then it will only match something that is `private static readonly`, and something that is just `private` will not match. +- `types` allows you to specify which types to match. This option supports simple, primitive types only (`boolean`, `string`, `number`, `array`, `function`). + - The name must match _one_ of the types. + - **_NOTE - Using this option will require that you lint with type information._** + - For example, this lets you do things like enforce that `boolean` variables are prefixed with a verb. + - `boolean` matches any type assignable to `boolean | null | undefined` + - `string` matches any type assignable to `string | null | undefined` + - `number` matches any type assignable to `number | null | undefined` + - `array` matches any type assignable to `Array | null | undefined` + - `function` matches any type assignable to `Function | null | undefined` + +The ordering of selectors does not matter. The implementation will automatically sort the selectors to ensure they match from most-specific to least specific. It will keep checking selectors in that order until it finds one that matches the name. + +For example, if you provide the following config: + +```ts +[ + /* 1 */ { selector: 'default', format: ['camelCase'] }, + /* 2 */ { selector: 'variable', format: ['snake_case'] }, + /* 3 */ { selector: 'variable', type: ['boolean'], format: ['UPPER_CASE'] }, + /* 4 */ { selector: 'variableLike', format: ['PascalCase'] }, +]; +``` + +Then for the code `const x = 1`, the rule will validate the selectors in the following order: `3`, `2`, `4`, `1`. + +#### Allowed Selectors, Modifiers and Types + +There are two types of selectors, individual selectors, and grouped selectors. + +##### Individual Selectors + +Individual Selectors match specific, well-defined sets. There is no overlap between each of the individual selectors. + +- `variable` - matches any `var` / `let` / `const` variable name. + - Allowed `modifiers`: none. + - Allowed `types`: `boolean`, `string`, `number`, `function`, `array`. +- `function` - matches any named function declaration or named function expression. + - Allowed `modifiers`: none. + - Allowed `types`: none. +- `parameter` - matches any function parameter. Does not match parameter properties. + - Allowed `modifiers`: none. + - Allowed `types`: `boolean`, `string`, `number`, `function`, `array`. +- `property` - matches any object, class, or object type property. Does not match properties that have direct function expression or arrow function expression values. + - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`. + - Allowed `types`: `boolean`, `string`, `number`, `function`, `array`. +- `parameterProperty` - matches any parameter property. + - Allowed `modifiers`: `private`, `protected`, `public`, `readonly`. + - Allowed `types`: `boolean`, `string`, `number`, `function`, `array`. +- `method` - matches any object, class, or object type method. Also matches properties that have direct function expression or arrow function expression values. Does not match accessors. + - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`. + - Allowed `types`: none. +- `accessor` - matches any accessor. + - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`. + - Allowed `types`: `boolean`, `string`, `number`, `function`, `array`. +- `enumMember` - matches any enum member. + - Allowed `modifiers`: none. + - Allowed `types`: none. +- `class` - matches any class declaration. + - Allowed `modifiers`: `abstract`. + - Allowed `types`: none. +- `interface` - matches any interface declaration. + - Allowed `modifiers`: none. + - Allowed `types`: none. +- `typeAlias` - matches any type alias declaration. + - Allowed `modifiers`: none. + - Allowed `types`: none. +- `enum` - matches any enum declaration. + - Allowed `modifiers`: none. + - Allowed `types`: none. +- `typeParameter` - matches any generic type parameter declaration. + - Allowed `modifiers`: none. + - Allowed `types`: none. + +##### Group Selectors + +Group Selectors are provided for convenience, and essentially bundle up sets of individual selectors. + +- `default` - matches everything. + - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`. + - Allowed `types`: none. +- `variableLike` - matches the same as `variable`, `function` and `parameter`. + - Allowed `modifiers`: none. + - Allowed `types`: none. +- `memberLike` - matches the same as `property`, `parameterProperty`, `method`, `accessor`, `enumMember`. + - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`. + - Allowed `types`: none. +- `typeLike` - matches the same as `class`, `interface`, `typeAlias`, `enum`, `typeParameter`. + - Allowed `modifiers`: `abstract`. + - Allowed `types`: none. + +## Examples + +### Enforce that all variables, functions and properties follow are camelCase + +```json +{ + "@typescript-eslint/naming-conventions": [ + "error", + { "selector": "variableLike", "format": ["camelCase"] } + ] +} +``` + +### Enforce that private members are prefixed with an underscore + +```json +{ + "@typescript-eslint/naming-conventions": [ + "error", + { + "selector": "memberLike", + "modifier": ["private"], + "format": ["camelCase"], + "leadingUnderscore": "require" + } + ] +} +``` + +### Enforce that boolean variables are prefixed with an allowed verb + +```json +{ + "@typescript-eslint/naming-conventions": [ + "error", + { + "selector": "variable", + "types": ["boolean"], + "format": ["PascalCase"], + "prefix": ["is", "should", "has", "can", "did", "will"] + } + ] +} +``` + +### Enforce that all variables are either in camelCase or UPPER_CASE + +```json +{ + "@typescript-eslint/naming-conventions": [ + "error", + { + "selector": "variable", + "format": ["camelCase", "UPPER_CASE"] + } + ] +} +``` + +### Enforce that type parameters (generics) are prefixed with `T` + +```json +{ + "@typescript-eslint/naming-conventions": [ + "error", + { + "selector": "typeParameter", + "format": ["PascalCase"], + "prefix": ["T"] + } + ] +} +``` + +### Enforce the codebase follows ESLint's `camelcase` conventions + +```json +{ + "@typescript-eslint/naming-conventions": [ + "error", + { + "selector": "default", + "format": ["camelCase"] + }, + + { + "selector": "variable", + "format": ["camelCase", "UPPER_CASE"] + }, + { + "selector": "parameter", + "format": ["camelCase"], + "leadingUnderscore": "allow" + }, + + { + "selector": "memberLike", + "modifiers": ["private"], + "format": ["camelCase"], + "leadingUnderscore": "require" + }, + + { + "selector": "typeLike", + "format": ["PascalCase"] + } + ] +} +``` + +## When Not To Use It + +If you do not want to enforce naming conventions for anything. diff --git a/packages/eslint-plugin/docs/rules/no-unnecessary-condition.md b/packages/eslint-plugin/docs/rules/no-unnecessary-condition.md index ae1e8eb0aa04..f2bea6f4fd84 100644 --- a/packages/eslint-plugin/docs/rules/no-unnecessary-condition.md +++ b/packages/eslint-plugin/docs/rules/no-unnecessary-condition.md @@ -74,6 +74,19 @@ for (; true; ) {} do {} while (true); ``` +- `checkArrayPredicates` (default: `false`) - if set checks that the return value from certain array method callbacks (`filter`, `find`, `some`, `every`) is necessarily conditional. + +```ts +// Valid: numbers can be truthy or falsy. +[0, 1, 2, 3].filter(t => t); + +// Invalid: arrays are always falsy. +[ + [1, 2], + [3, 4], +].filter(t => t); +``` + ## When Not To Use It The main downside to using this rule is the need for type information. diff --git a/packages/eslint-plugin/docs/rules/require-array-sort-compare.md b/packages/eslint-plugin/docs/rules/require-array-sort-compare.md index 5b30533a9e9f..3a939f7d149b 100644 --- a/packages/eslint-plugin/docs/rules/require-array-sort-compare.md +++ b/packages/eslint-plugin/docs/rules/require-array-sort-compare.md @@ -1,21 +1,23 @@ -# Enforce giving `compare` argument to `Array#sort` (`require-array-sort-compare`) +# Requires `Array#sort` calls to always provide a `compareFunction` (`require-array-sort-compare`) -This rule prevents to invoke `Array#sort()` method without `compare` argument. +This rule prevents invoking the `Array#sort()` method without providing a `compare` argument. -`Array#sort()` method sorts that element by the alphabet order. +When called without a compare function, `Array#sort()` converts all non-undefined array elements into strings and then compares said strings based off their UTF-16 code units. + +The result is that elements are sorted alphabetically, regardless of their type. +When sorting numbers, this results in the classic "10 before 2" order: ```ts [1, 2, 3, 10, 20, 30].sort(); //→ [1, 10, 2, 20, 3, 30] ``` -The language specification also noted this trap. +This also means that `Array#sort` does not always sort consistently, as elements may have custom `#toString` implementations that are not deterministic; this trap is noted in the noted in the language specification thusly: > NOTE 2: Method calls performed by the `ToString` abstract operations in steps 5 and 7 have the potential to cause `SortCompare` to not behave as a consistent comparison function.
> https://www.ecma-international.org/ecma-262/9.0/#sec-sortcompare ## Rule Details -This rule is aimed at preventing the calls of `Array#sort` method. -This rule ignores the `sort` methods of user-defined types. +This rule aims to ensure all calls of the native `Array#sort` method provide a `compareFunction`, while ignoring calls to user-defined `sort` methods. Examples of **incorrect** code for this rule: @@ -25,7 +27,7 @@ const stringArray: string[]; array.sort(); -// Even if a string array, warns it in favor of `String#localeCompare` method. +// String arrays should be sorted using `String#localeCompare`. stringArray.sort(); ``` @@ -41,9 +43,9 @@ array.sort((a, b) => a.localeCompare(b)); userDefinedType.sort(); ``` -### Options +## Options -There is no option. +None. ## When Not To Use It diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index 572d0e3101ca..7049cb7ba6e4 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/eslint-plugin", - "version": "2.15.0", + "version": "2.16.0", "description": "TypeScript plugin for ESLint", "keywords": [ "eslint", @@ -30,8 +30,8 @@ "main": "dist/index.js", "scripts": { "build": "tsc -b tsconfig.build.json", - "check:docs": "../../node_modules/.bin/ts-node --files --transpile-only ./tools/validate-docs/index.ts", - "check:configs": "../../node_modules/.bin/ts-node --files --transpile-only ./tools/validate-configs/index.ts", + "check:docs": "jest tests/docs.test.ts --runTestsByPath --silent --runInBand", + "check:configs": "jest tests/configs.test.ts --runTestsByPath --silent --runInBand", "clean": "tsc -b tsconfig.build.json --clean", "format": "prettier --write \"./**/*.{ts,js,json,md}\" --ignore-path ../../.prettierignore", "generate:configs": "../../node_modules/.bin/ts-node --files --transpile-only tools/generate-configs.ts", @@ -40,7 +40,7 @@ "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { - "@typescript-eslint/experimental-utils": "2.15.0", + "@typescript-eslint/experimental-utils": "2.16.0", "eslint-utils": "^1.4.3", "functional-red-black-tree": "^1.0.1", "regexpp": "^3.0.0", diff --git a/packages/eslint-plugin/src/configs/all.json b/packages/eslint-plugin/src/configs/all.json index f9eed1fca9cf..b8eff6e78b95 100644 --- a/packages/eslint-plugin/src/configs/all.json +++ b/packages/eslint-plugin/src/configs/all.json @@ -8,22 +8,18 @@ "@typescript-eslint/ban-types": "error", "brace-style": "off", "@typescript-eslint/brace-style": "error", - "camelcase": "off", - "@typescript-eslint/camelcase": "error", - "@typescript-eslint/class-name-casing": "error", "@typescript-eslint/consistent-type-assertions": "error", "@typescript-eslint/consistent-type-definitions": "error", + "@typescript-eslint/default-param-last": "error", "@typescript-eslint/explicit-function-return-type": "error", "@typescript-eslint/explicit-member-accessibility": "error", "func-call-spacing": "off", "@typescript-eslint/func-call-spacing": "error", - "@typescript-eslint/generic-type-naming": "error", "indent": "off", "@typescript-eslint/indent": "error", - "@typescript-eslint/interface-name-prefix": "error", "@typescript-eslint/member-delimiter-style": "error", - "@typescript-eslint/member-naming": "error", "@typescript-eslint/member-ordering": "error", + "@typescript-eslint/naming-convention": "error", "no-array-constructor": "off", "@typescript-eslint/no-array-constructor": "error", "@typescript-eslint/no-dynamic-delete": "error", diff --git a/packages/eslint-plugin/src/configs/eslint-recommended.ts b/packages/eslint-plugin/src/configs/eslint-recommended.ts index 39e8379b56b0..696cd612d2de 100644 --- a/packages/eslint-plugin/src/configs/eslint-recommended.ts +++ b/packages/eslint-plugin/src/configs/eslint-recommended.ts @@ -7,7 +7,7 @@ export default { { files: ['*.ts', '*.tsx'], rules: { - //Checked by Typescript - ts(2378) + // Checked by Typescript - ts(2378) 'getter-return': 'off', // Checked by Typescript - ts(2300) 'no-dupe-args': 'off', diff --git a/packages/eslint-plugin/src/rules/await-thenable.ts b/packages/eslint-plugin/src/rules/await-thenable.ts index f5b06b683f46..78b7ad129393 100644 --- a/packages/eslint-plugin/src/rules/await-thenable.ts +++ b/packages/eslint-plugin/src/rules/await-thenable.ts @@ -26,9 +26,7 @@ export default util.createRule({ return { AwaitExpression(node): void { - const originalNode = parserServices.esTreeNodeToTSNodeMap.get< - ts.AwaitExpression - >(node); + const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node); const type = checker.getTypeAtLocation(originalNode.expression); if ( diff --git a/packages/eslint-plugin/src/rules/camelcase.ts b/packages/eslint-plugin/src/rules/camelcase.ts index 17b88d7e93d9..37047b9b4ff3 100644 --- a/packages/eslint-plugin/src/rules/camelcase.ts +++ b/packages/eslint-plugin/src/rules/camelcase.ts @@ -30,6 +30,8 @@ export default util.createRule({ category: 'Stylistic Issues', recommended: 'error', }, + deprecated: true, + replacedBy: ['naming-convention'], schema: [schema], messages: baseRule.meta.messages, }, diff --git a/packages/eslint-plugin/src/rules/class-name-casing.ts b/packages/eslint-plugin/src/rules/class-name-casing.ts index cbcc805630d4..6326f1910e18 100644 --- a/packages/eslint-plugin/src/rules/class-name-casing.ts +++ b/packages/eslint-plugin/src/rules/class-name-casing.ts @@ -20,6 +20,8 @@ export default util.createRule({ category: 'Best Practices', recommended: 'error', }, + deprecated: true, + replacedBy: ['naming-convention'], messages: { notPascalCased: "{{friendlyName}} '{{name}}' must be PascalCased.", }, diff --git a/packages/eslint-plugin/src/rules/default-param-last.ts b/packages/eslint-plugin/src/rules/default-param-last.ts new file mode 100644 index 000000000000..0f0191155a47 --- /dev/null +++ b/packages/eslint-plugin/src/rules/default-param-last.ts @@ -0,0 +1,76 @@ +import { createRule } from '../util'; +import { + TSESTree, + AST_NODE_TYPES, +} from '@typescript-eslint/experimental-utils'; + +export default createRule({ + name: 'default-param-last', + meta: { + type: 'suggestion', + docs: { + description: 'Enforce default parameters to be last', + category: 'Best Practices', + recommended: false, + }, + schema: [], + messages: { + shouldBeLast: 'Default parameters should be last.', + }, + }, + defaultOptions: [], + create(context) { + /** + * checks if node is optional parameter + * @param node the node to be evaluated + * @private + */ + function isOptionalParam(node: TSESTree.Parameter): boolean { + return 'optional' in node && node.optional === true; + } + + /** + * checks if node is plain parameter + * @param node the node to be evaluated + * @private + */ + function isPlainParam(node: TSESTree.Parameter): boolean { + return !( + node.type === AST_NODE_TYPES.AssignmentPattern || + node.type === AST_NODE_TYPES.RestElement || + isOptionalParam(node) + ); + } + + function checkDefaultParamLast( + node: + | TSESTree.ArrowFunctionExpression + | TSESTree.FunctionDeclaration + | TSESTree.FunctionExpression, + ): void { + let hasSeenPlainParam = false; + for (let i = node.params.length - 1; i >= 0; i--) { + const param = node.params[i]; + + if (isPlainParam(param)) { + hasSeenPlainParam = true; + continue; + } + + if ( + hasSeenPlainParam && + (isOptionalParam(param) || + param.type === AST_NODE_TYPES.AssignmentPattern) + ) { + context.report({ node: param, messageId: 'shouldBeLast' }); + } + } + } + + return { + ArrowFunctionExpression: checkDefaultParamLast, + FunctionDeclaration: checkDefaultParamLast, + FunctionExpression: checkDefaultParamLast, + }; + }, +}); diff --git a/packages/eslint-plugin/src/rules/generic-type-naming.ts b/packages/eslint-plugin/src/rules/generic-type-naming.ts index 95516174448f..14a697f2e604 100644 --- a/packages/eslint-plugin/src/rules/generic-type-naming.ts +++ b/packages/eslint-plugin/src/rules/generic-type-naming.ts @@ -13,6 +13,8 @@ export default util.createRule({ // too opinionated to be recommended recommended: false, }, + deprecated: true, + replacedBy: ['naming-convention'], messages: { paramNotMatchRule: 'Type parameter {{name}} does not match rule {{rule}}.', diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index 5d2f53cce251..de4aa662ea32 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -8,6 +8,7 @@ import camelcase from './camelcase'; import classNameCasing from './class-name-casing'; import consistentTypeAssertions from './consistent-type-assertions'; import consistentTypeDefinitions from './consistent-type-definitions'; +import defaultParamLast from './default-param-last'; import explicitFunctionReturnType from './explicit-function-return-type'; import explicitMemberAccessibility from './explicit-member-accessibility'; import funcCallSpacing from './func-call-spacing'; @@ -17,6 +18,7 @@ import interfaceNamePrefix from './interface-name-prefix'; import memberDelimiterStyle from './member-delimiter-style'; import memberNaming from './member-naming'; import memberOrdering from './member-ordering'; +import namingConvention from './naming-convention'; import noArrayConstructor from './no-array-constructor'; import noDynamicDelete from './no-dynamic-delete'; import noEmptyFunction from './no-empty-function'; @@ -87,6 +89,7 @@ export default { 'class-name-casing': classNameCasing, 'consistent-type-assertions': consistentTypeAssertions, 'consistent-type-definitions': consistentTypeDefinitions, + 'default-param-last': defaultParamLast, 'explicit-function-return-type': explicitFunctionReturnType, 'explicit-member-accessibility': explicitMemberAccessibility, 'func-call-spacing': funcCallSpacing, @@ -96,6 +99,7 @@ export default { 'member-delimiter-style': memberDelimiterStyle, 'member-naming': memberNaming, 'member-ordering': memberOrdering, + 'naming-convention': namingConvention, 'no-array-constructor': noArrayConstructor, 'no-dynamic-delete': noDynamicDelete, 'no-empty-function': noEmptyFunction, diff --git a/packages/eslint-plugin/src/rules/interface-name-prefix.ts b/packages/eslint-plugin/src/rules/interface-name-prefix.ts index 6f2c57dc3159..039c0d7d67c2 100644 --- a/packages/eslint-plugin/src/rules/interface-name-prefix.ts +++ b/packages/eslint-plugin/src/rules/interface-name-prefix.ts @@ -49,6 +49,8 @@ export default util.createRule({ // https://github.com/typescript-eslint/typescript-eslint/issues/374 recommended: 'error', }, + deprecated: true, + replacedBy: ['naming-convention'], messages: { noPrefix: 'Interface name must not be prefixed with "I".', alwaysPrefix: 'Interface name must be prefixed with "I".', diff --git a/packages/eslint-plugin/src/rules/member-naming.ts b/packages/eslint-plugin/src/rules/member-naming.ts index f2e4567dcb40..4facce887e5f 100644 --- a/packages/eslint-plugin/src/rules/member-naming.ts +++ b/packages/eslint-plugin/src/rules/member-naming.ts @@ -23,6 +23,8 @@ export default util.createRule({ category: 'Stylistic Issues', recommended: false, }, + deprecated: true, + replacedBy: ['naming-convention'], messages: { incorrectName: '{{accessibility}} property {{name}} should match {{convention}}.', diff --git a/packages/eslint-plugin/src/rules/naming-convention.ts b/packages/eslint-plugin/src/rules/naming-convention.ts new file mode 100644 index 000000000000..50acf190d046 --- /dev/null +++ b/packages/eslint-plugin/src/rules/naming-convention.ts @@ -0,0 +1,1294 @@ +import { + AST_NODE_TYPES, + JSONSchema, + TSESTree, + TSESLint, +} from '@typescript-eslint/experimental-utils'; +import * as ts from 'typescript'; +import * as util from '../util'; + +type MessageIds = + | 'unexpectedUnderscore' + | 'missingUnderscore' + | 'missingAffix' + | 'satisfyCustom' + | 'doesNotMatchFormat'; + +// #region Options Type Config + +enum PredefinedFormats { + camelCase = 1 << 0, + strictCamelCase = 1 << 1, + PascalCase = 1 << 2, + StrictPascalCase = 1 << 3, + // eslint-disable-next-line @typescript-eslint/camelcase + snake_case = 1 << 4, + UPPER_CASE = 1 << 5, +} +type PredefinedFormatsString = keyof typeof PredefinedFormats; + +enum UnderscoreOptions { + forbid = 1 << 0, + allow = 1 << 1, + require = 1 << 2, +} +type UnderscoreOptionsString = keyof typeof UnderscoreOptions; + +enum Selectors { + // variableLike + variable = 1 << 0, + function = 1 << 1, + parameter = 1 << 2, + + // memberLike + property = 1 << 3, + parameterProperty = 1 << 4, + method = 1 << 5, + accessor = 1 << 6, + enumMember = 1 << 7, + + // typeLike + class = 1 << 8, + interface = 1 << 9, + typeAlias = 1 << 10, + enum = 1 << 11, + typeParameter = 1 << 12, +} +type SelectorsString = keyof typeof Selectors; +const SELECTOR_COUNT = util.getEnumNames(Selectors).length; + +enum MetaSelectors { + default = -1, + variableLike = 0 | + Selectors.variable | + Selectors.function | + Selectors.parameter, + memberLike = 0 | + Selectors.property | + Selectors.parameterProperty | + Selectors.enumMember | + Selectors.method | + Selectors.accessor, + typeLike = 0 | + Selectors.class | + Selectors.interface | + Selectors.typeAlias | + Selectors.enum | + Selectors.typeParameter, +} +type MetaSelectorsString = keyof typeof MetaSelectors; +type IndividualAndMetaSelectorsString = SelectorsString | MetaSelectorsString; + +enum Modifiers { + readonly = 1 << 0, + static = 1 << 1, + public = 1 << 2, + protected = 1 << 3, + private = 1 << 4, + abstract = 1 << 5, +} +type ModifiersString = keyof typeof Modifiers; + +enum TypeModifiers { + boolean = 1 << 10, + string = 1 << 11, + number = 1 << 12, + function = 1 << 13, + array = 1 << 14, +} +type TypeModifiersString = keyof typeof TypeModifiers; + +interface Selector { + // format options + format: PredefinedFormatsString[]; + custom?: { + regex: string; + match: boolean; + }; + leadingUnderscore?: UnderscoreOptionsString; + trailingUnderscore?: UnderscoreOptionsString; + prefix?: string[]; + suffix?: string[]; + // selector options + selector: IndividualAndMetaSelectorsString; + modifiers?: ModifiersString[]; + types?: TypeModifiersString[]; + filter?: string; +} +interface NormalizedSelector { + // format options + format: PredefinedFormats[]; + custom: { + regex: RegExp; + match: boolean; + } | null; + leadingUnderscore: UnderscoreOptions | null; + trailingUnderscore: UnderscoreOptions | null; + prefix: string[] | null; + suffix: string[] | null; + // selector options + selector: Selectors | MetaSelectors; + modifiers: Modifiers[] | null; + types: TypeModifiers[] | null; + filter: RegExp | null; + // calculated ordering weight based on modifiers + modifierWeight: number; +} + +// Note that this intentionally does not strictly type the modifiers/types properties. +// This is because doing so creates a huge headache, as the rule's code doesn't need to care. +// The JSON Schema strictly types these properties, so we know the user won't input invalid config. +type Options = Selector[]; + +// #endregion Options Type Config + +// #region Schema Config + +const UNDERSCORE_SCHEMA: JSONSchema.JSONSchema4 = { + type: 'string', + enum: util.getEnumNames(UnderscoreOptions), +}; +const PREFIX_SUFFIX_SCHEMA: JSONSchema.JSONSchema4 = { + type: 'array', + items: { + type: 'string', + minLength: 1, + }, + minItems: 1, + additionalItems: false, +}; +type JSONSchemaProperties = Record; +const FORMAT_OPTIONS_PROPERTIES: JSONSchemaProperties = { + format: { + type: 'array', + items: { + type: 'string', + enum: util.getEnumNames(PredefinedFormats), + }, + minItems: 1, + additionalItems: false, + }, + custom: { + type: 'object', + properties: { + regex: { + type: 'string', + }, + match: { + type: 'boolean', + }, + }, + required: ['regex', 'match'], + }, + leadingUnderscore: UNDERSCORE_SCHEMA, + trailingUnderscore: UNDERSCORE_SCHEMA, + prefix: PREFIX_SUFFIX_SCHEMA, + suffix: PREFIX_SUFFIX_SCHEMA, +}; +function selectorSchema( + selectorString: IndividualAndMetaSelectorsString, + allowType: boolean, + modifiers?: ModifiersString[], +): JSONSchema.JSONSchema4[] { + const selector: JSONSchemaProperties = { + filter: { + type: 'string', + minLength: 1, + }, + selector: { + type: 'string', + enum: [selectorString], + }, + }; + if (modifiers && modifiers.length > 0) { + selector.modifiers = { + type: 'array', + items: { + type: 'string', + enum: modifiers, + }, + additionalItems: false, + }; + } + if (allowType) { + selector.types = { + type: 'array', + items: { + type: 'string', + enum: util.getEnumNames(TypeModifiers), + }, + additionalItems: false, + }; + } + + return [ + { + type: 'object', + properties: { + ...FORMAT_OPTIONS_PROPERTIES, + ...selector, + }, + required: ['selector', 'format'], + additionalProperties: false, + }, + ]; +} +const SCHEMA: JSONSchema.JSONSchema4 = { + type: 'array', + minItems: 1, + items: { + oneOf: [ + ...selectorSchema('default', false, util.getEnumNames(Modifiers)), + + ...selectorSchema('variableLike', false), + ...selectorSchema('variable', true), + ...selectorSchema('function', false), + ...selectorSchema('parameter', true), + + ...selectorSchema('memberLike', false, [ + 'private', + 'protected', + 'public', + 'static', + 'readonly', + 'abstract', + ]), + ...selectorSchema('property', true, [ + 'private', + 'protected', + 'public', + 'static', + 'readonly', + 'abstract', + ]), + ...selectorSchema('parameterProperty', true, [ + 'private', + 'protected', + 'public', + 'readonly', + ]), + ...selectorSchema('method', false, [ + 'private', + 'protected', + 'public', + 'static', + 'abstract', + ]), + ...selectorSchema('accessor', true, [ + 'private', + 'protected', + 'public', + 'static', + 'abstract', + ]), + ...selectorSchema('enumMember', false), + + ...selectorSchema('typeLike', false, ['abstract']), + ...selectorSchema('class', false, ['abstract']), + ...selectorSchema('interface', false), + ...selectorSchema('typeAlias', false), + ...selectorSchema('enum', false), + ...selectorSchema('typeParameter', false), + ], + }, + additionalItems: false, +}; + +// #endregion Schema Config + +// This essentially mirrors ESLint's `camelcase` rule +// note that that rule ignores leading and trailing underscores and only checks those in the middle of a variable name +const defaultCamelCaseAllTheThingsConfig: Options = [ + { + selector: 'default', + format: ['camelCase'], + leadingUnderscore: 'allow', + trailingUnderscore: 'allow', + }, + + { + selector: 'variable', + format: ['camelCase', 'UPPER_CASE'], + leadingUnderscore: 'allow', + trailingUnderscore: 'allow', + }, + + { + selector: 'typeLike', + format: ['PascalCase'], + }, +]; + +export default util.createRule({ + name: 'naming-convention', + meta: { + docs: { + category: 'Variables', + description: + 'Enforces naming conventions for everything across a codebase', + recommended: false, + // technically only requires type checking if the user uses "type" modifiers + requiresTypeChecking: true, + }, + type: 'suggestion', + messages: { + unexpectedUnderscore: + '{{type}} name {{name}} must not have a {{position}} underscore.', + missingUnderscore: + '{{type}} name {{name}} must have a {{position}} underscore', + missingAffix: + '{{type}} name {{name}} must have one of the following {{position}}es: {{affixes}}', + satisfyCustom: + '{{type}} name {{name}} must {{regexMatch}} the RegExp: {{regex}}', + doesNotMatchFormat: + '{{type}} name {{name}} must match one of the following formats: {{formats}}', + }, + schema: SCHEMA, + }, + defaultOptions: defaultCamelCaseAllTheThingsConfig, + create(contextWithoutDefaults) { + const context: Context = contextWithoutDefaults.options + ? contextWithoutDefaults + : // only apply the defaults when the user provides no config + Object.setPrototypeOf( + { + options: defaultCamelCaseAllTheThingsConfig, + }, + contextWithoutDefaults, + ); + + const validators = parseOptions(context); + + function handleMember( + validator: ValidatorFunction | null, + node: + | TSESTree.PropertyNonComputedName + | TSESTree.ClassPropertyNonComputedName + | TSESTree.TSAbstractClassPropertyNonComputedName + | TSESTree.TSPropertySignatureNonComputedName + | TSESTree.MethodDefinitionNonComputedName + | TSESTree.TSAbstractMethodDefinitionNonComputedName + | TSESTree.TSMethodSignatureNonComputedName, + modifiers: Set, + ): void { + if (!validator) { + return; + } + + const key = node.key; + validator(key, modifiers); + } + + function getMemberModifiers( + node: + | TSESTree.ClassProperty + | TSESTree.TSAbstractClassProperty + | TSESTree.MethodDefinition + | TSESTree.TSAbstractMethodDefinition + | TSESTree.TSParameterProperty, + ): Set { + const modifiers = new Set(); + if (node.accessibility) { + modifiers.add(Modifiers[node.accessibility]); + } else { + modifiers.add(Modifiers.public); + } + if (node.static) { + modifiers.add(Modifiers.static); + } + if ('readonly' in node && node.readonly) { + modifiers.add(Modifiers.readonly); + } + if ( + node.type === AST_NODE_TYPES.TSAbstractClassProperty || + node.type === AST_NODE_TYPES.TSAbstractMethodDefinition + ) { + modifiers.add(Modifiers.abstract); + } + + return modifiers; + } + + return { + // #region variable + + VariableDeclarator(node: TSESTree.VariableDeclarator): void { + const validator = validators.variable; + if (!validator) { + return; + } + + const identifiers: TSESTree.Identifier[] = []; + getIdentifiersFromPattern(node.id, identifiers); + + identifiers.forEach(i => { + validator(i); + }); + }, + + // #endregion + + // #region function + + 'FunctionDeclaration, TSDeclareFunction, FunctionExpression'( + node: + | TSESTree.FunctionDeclaration + | TSESTree.TSDeclareFunction + | TSESTree.FunctionExpression, + ): void { + const validator = validators.function; + if (!validator || node.id === null) { + return; + } + + validator(node.id); + }, + + // #endregion function + + // #region parameter + + 'FunctionDeclaration, TSDeclareFunction, FunctionExpression, ArrowFunctionExpression'( + node: + | TSESTree.FunctionDeclaration + | TSESTree.FunctionExpression + | TSESTree.ArrowFunctionExpression, + ): void { + const validator = validators.parameter; + if (!validator) { + return; + } + + node.params.forEach(param => { + if (param.type === AST_NODE_TYPES.TSParameterProperty) { + return; + } + + const identifiers: TSESTree.Identifier[] = []; + getIdentifiersFromPattern(param, identifiers); + + identifiers.forEach(i => { + validator(i); + }); + }); + }, + + // #endregion parameter + + // #region parameterProperty + + TSParameterProperty(node): void { + const validator = validators.parameterProperty; + if (!validator) { + return; + } + + const modifiers = getMemberModifiers(node); + + const identifiers: TSESTree.Identifier[] = []; + getIdentifiersFromPattern(node.parameter, identifiers); + + identifiers.forEach(i => { + validator(i, modifiers); + }); + }, + + // #endregion parameterProperty + + // #region property + + 'Property[computed = false][kind = "init"][value.type != "ArrowFunctionExpression"][value.type != "FunctionExpression"][value.type != "TSEmptyBodyFunctionExpression"]'( + node: TSESTree.PropertyNonComputedName, + ): void { + const modifiers = new Set([Modifiers.public]); + handleMember(validators.property, node, modifiers); + }, + + ':matches(ClassProperty, TSAbstractClassProperty)[computed = false][value.type != "ArrowFunctionExpression"][value.type != "FunctionExpression"][value.type != "TSEmptyBodyFunctionExpression"]'( + node: + | TSESTree.ClassPropertyNonComputedName + | TSESTree.TSAbstractClassPropertyNonComputedName, + ): void { + const modifiers = getMemberModifiers(node); + handleMember(validators.property, node, modifiers); + }, + + 'TSPropertySignature[computed = false]'( + node: TSESTree.TSPropertySignatureNonComputedName, + ): void { + const modifiers = new Set([Modifiers.public]); + if (node.readonly) { + modifiers.add(Modifiers.readonly); + } + + handleMember(validators.property, node, modifiers); + }, + + // #endregion property + + // #region method + + [[ + 'Property[computed = false][kind = "init"][value.type = "ArrowFunctionExpression"]', + 'Property[computed = false][kind = "init"][value.type = "FunctionExpression"]', + 'Property[computed = false][kind = "init"][value.type = "TSEmptyBodyFunctionExpression"]', + 'TSMethodSignature[computed = false]', + ].join(', ')]( + node: + | TSESTree.PropertyNonComputedName + | TSESTree.TSMethodSignatureNonComputedName, + ): void { + const modifiers = new Set([Modifiers.public]); + handleMember(validators.method, node, modifiers); + }, + + [[ + ':matches(ClassProperty, TSAbstractClassProperty)[computed = false][value.type = "ArrowFunctionExpression"]', + ':matches(ClassProperty, TSAbstractClassProperty)[computed = false][value.type = "FunctionExpression"]', + ':matches(ClassProperty, TSAbstractClassProperty)[computed = false][value.type = "TSEmptyBodyFunctionExpression"]', + ':matches(MethodDefinition, TSAbstractMethodDefinition)[computed = false][kind = "method"]', + ].join(', ')]( + node: + | TSESTree.ClassPropertyNonComputedName + | TSESTree.TSAbstractClassPropertyNonComputedName + | TSESTree.MethodDefinitionNonComputedName + | TSESTree.TSAbstractMethodDefinitionNonComputedName, + ): void { + const modifiers = getMemberModifiers(node); + handleMember(validators.method, node, modifiers); + }, + + // #endregion method + + // #region accessor + + 'Property[computed = false]:matches([kind = "get"], [kind = "set"])'( + node: TSESTree.PropertyNonComputedName, + ): void { + const modifiers = new Set([Modifiers.public]); + handleMember(validators.accessor, node, modifiers); + }, + + 'MethodDefinition[computed = false]:matches([kind = "get"], [kind = "set"])'( + node: TSESTree.MethodDefinitionNonComputedName, + ): void { + const modifiers = getMemberModifiers(node); + handleMember(validators.accessor, node, modifiers); + }, + + // #endregion accessor + + // #region enumMember + + // computed is optional, so can't do [computed = false] + 'TSEnumMember[computed != true]'( + node: TSESTree.TSEnumMemberNonComputedName, + ): void { + const validator = validators.enumMember; + if (!validator) { + return; + } + + const id = node.id; + validator(id); + }, + + // #endregion enumMember + + // #region class + + 'ClassDeclaration, ClassExpression'( + node: TSESTree.ClassDeclaration | TSESTree.ClassExpression, + ): void { + const validator = validators.class; + if (!validator) { + return; + } + + const id = node.id; + if (id === null) { + return; + } + + const modifiers = new Set(); + if (node.abstract) { + modifiers.add(Modifiers.abstract); + } + + validator(id, modifiers); + }, + + // #endregion class + + // #region interface + + TSInterfaceDeclaration(node): void { + const validator = validators.interface; + if (!validator) { + return; + } + + validator(node.id); + }, + + // #endregion interface + + // #region typeAlias + + TSTypeAliasDeclaration(node): void { + const validator = validators.typeAlias; + if (!validator) { + return; + } + + validator(node.id); + }, + + // #endregion typeAlias + + // #region enum + + TSEnumDeclaration(node): void { + const validator = validators.enum; + if (!validator) { + return; + } + + validator(node.id); + }, + + // #endregion enum + + // #region typeParameter + + 'TSTypeParameterDeclaration > TSTypeParameter'( + node: TSESTree.TSTypeParameter, + ): void { + const validator = validators.typeParameter; + if (!validator) { + return; + } + + validator(node.name); + }, + + // #endregion typeParameter + }; + }, +}); + +function getIdentifiersFromPattern( + pattern: TSESTree.DestructuringPattern, + identifiers: TSESTree.Identifier[], +): void { + switch (pattern.type) { + case AST_NODE_TYPES.Identifier: + identifiers.push(pattern); + break; + + case AST_NODE_TYPES.ArrayPattern: + pattern.elements.forEach(element => { + getIdentifiersFromPattern(element, identifiers); + }); + break; + + case AST_NODE_TYPES.ObjectPattern: + pattern.properties.forEach(property => { + if (property.type === AST_NODE_TYPES.RestElement) { + getIdentifiersFromPattern(property, identifiers); + } else { + // this is a bit weird, but it's because ESTree doesn't have a new node type + // for object destructuring properties - it just reuses Property... + // https://github.com/estree/estree/blob/9ae284b71130d53226e7153b42f01bf819e6e657/es2015.md#L206-L211 + // However, the parser guarantees this is safe (and there is error handling) + getIdentifiersFromPattern( + property.value as TSESTree.DestructuringPattern, + identifiers, + ); + } + }); + break; + + case AST_NODE_TYPES.RestElement: + getIdentifiersFromPattern(pattern.argument, identifiers); + break; + + case AST_NODE_TYPES.AssignmentPattern: + getIdentifiersFromPattern(pattern.left, identifiers); + break; + + case AST_NODE_TYPES.MemberExpression: + // ignore member expressions, as the everything must already be defined + break; + + default: + // https://github.com/typescript-eslint/typescript-eslint/issues/1282 + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + throw new Error(`Unexpected pattern type ${pattern!.type}`); + } +} + +type ValidatorFunction = ( + node: TSESTree.Identifier | TSESTree.Literal, + modifiers?: Set, +) => void; +type ParsedOptions = Record; +type Context = TSESLint.RuleContext; +function parseOptions(context: Context): ParsedOptions { + const normalizedOptions = context.options.map(opt => normalizeOption(opt)); + const parsedOptions = util.getEnumNames(Selectors).reduce((acc, k) => { + acc[k] = createValidator(k, context, normalizedOptions); + return acc; + }, {} as ParsedOptions); + + return parsedOptions; +} +function createValidator( + type: SelectorsString, + context: Context, + allConfigs: NormalizedSelector[], +): (node: TSESTree.Identifier | TSESTree.Literal) => void { + // make sure the "highest priority" configs are checked first + const selectorType = Selectors[type]; + const configs = allConfigs + // gather all of the applicable selectors + .filter( + c => + (c.selector & selectorType) !== 0 || + c.selector === MetaSelectors.default, + ) + .sort((a, b) => { + if (a.selector === b.selector) { + // in the event of the same selector, order by modifier weight + // sort ascending - the type modifiers are "more important" + return a.modifierWeight - b.modifierWeight; + } + + /* + meta selectors will always be larger numbers than the normal selectors they contain, as they are the sum of all + of the selectors that they contain. + to give normal selectors a higher priority, shift them all SELECTOR_COUNT bits to the left before comparison, so + they are instead always guaranteed to be larger than the meta selectors. + */ + const aSelector = isMetaSelector(a.selector) + ? a.selector + : a.selector << SELECTOR_COUNT; + const bSelector = isMetaSelector(b.selector) + ? b.selector + : b.selector << SELECTOR_COUNT; + + // sort descending - the meta selectors are "least important" + return bSelector - aSelector; + }); + + return ( + node: TSESTree.Identifier | TSESTree.Literal, + modifiers: Set = new Set(), + ): void => { + const originalName = + node.type === AST_NODE_TYPES.Identifier ? node.name : `${node.value}`; + + // return will break the loop and stop checking configs + // it is only used when the name is known to have failed or succeeded a config. + for (const config of configs) { + if (config.filter?.test(originalName)) { + // name does not match the filter + continue; + } + + if (config.modifiers?.some(modifier => !modifiers.has(modifier))) { + // does not have the required modifiers + continue; + } + + if (!isCorrectType(node, config, context)) { + // is not the correct type + continue; + } + + let name: string | null = originalName; + + name = validateUnderscore('leading', config, name, node, originalName); + if (name === null) { + // fail + return; + } + + name = validateUnderscore('trailing', config, name, node, originalName); + if (name === null) { + // fail + return; + } + + name = validateAffix('prefix', config, name, node, originalName); + if (name === null) { + // fail + return; + } + + name = validateAffix('suffix', config, name, node, originalName); + if (name === null) { + // fail + return; + } + + if (!validateCustom(config, name, node, originalName)) { + // fail + return; + } + + if (!validatePredefinedFormat(config, name, node, originalName)) { + // fail + return; + } + + // it's valid for this config, so we don't need to check any more configs + return; + } + }; + + // centralizes the logic for formatting the report data + function formatReportData({ + affixes, + formats, + originalName, + position, + custom, + }: { + affixes?: string[]; + formats?: PredefinedFormats[]; + originalName: string; + position?: 'leading' | 'trailing' | 'prefix' | 'suffix'; + custom?: NonNullable; + }): Record { + return { + type: selectorTypeToMessageString(type), + name: originalName, + position, + affixes: affixes?.join(', '), + formats: formats?.map(f => PredefinedFormats[f]).join(', '), + regex: custom?.regex?.toString(), + regexMatch: + custom?.match === true + ? 'match' + : custom?.match === false + ? 'not match' + : null, + }; + } + + /** + * @returns the name with the underscore removed, if it is valid according to the specified underscore option, null otherwise + */ + function validateUnderscore( + position: 'leading' | 'trailing', + config: NormalizedSelector, + name: string, + node: TSESTree.Identifier | TSESTree.Literal, + originalName: string, + ): string | null { + const option = + position === 'leading' + ? config.leadingUnderscore + : config.trailingUnderscore; + if (!option) { + return name; + } + + const hasUnderscore = + position === 'leading' ? name.startsWith('_') : name.endsWith('_'); + const trimUnderscore = + position === 'leading' + ? (): string => name.slice(1) + : (): string => name.slice(0, -1); + + switch (option) { + case UnderscoreOptions.allow: + // no check - the user doesn't care if it's there or not + break; + + case UnderscoreOptions.forbid: + if (hasUnderscore) { + context.report({ + node, + messageId: 'unexpectedUnderscore', + data: formatReportData({ + originalName, + position, + }), + }); + return null; + } + break; + + case UnderscoreOptions.require: + if (!hasUnderscore) { + context.report({ + node, + messageId: 'missingUnderscore', + data: formatReportData({ + originalName, + position, + }), + }); + return null; + } + } + + return hasUnderscore ? trimUnderscore() : name; + } + + /** + * @returns the name with the affix removed, if it is valid according to the specified affix option, null otherwise + */ + function validateAffix( + position: 'prefix' | 'suffix', + config: NormalizedSelector, + name: string, + node: TSESTree.Identifier | TSESTree.Literal, + originalName: string, + ): string | null { + const affixes = config[position]; + if (!affixes || affixes.length === 0) { + return name; + } + + for (const affix of affixes) { + const hasAffix = + position === 'prefix' ? name.startsWith(affix) : name.endsWith(affix); + const trimAffix = + position === 'prefix' + ? (): string => name.slice(affix.length) + : (): string => name.slice(0, -affix.length); + + if (hasAffix) { + // matches, so trim it and return + return trimAffix(); + } + } + + context.report({ + node, + messageId: 'missingAffix', + data: formatReportData({ + originalName, + position, + affixes, + }), + }); + return null; + } + + /** + * @returns true if the name is valid according to the `regex` option, false otherwise + */ + function validateCustom( + config: NormalizedSelector, + name: string, + node: TSESTree.Identifier | TSESTree.Literal, + originalName: string, + ): boolean { + const custom = config.custom; + if (!custom) { + return true; + } + + const result = custom.regex.test(name); + if (custom.match && result) { + return true; + } + if (!custom.match && !result) { + return true; + } + + context.report({ + node, + messageId: 'satisfyCustom', + data: formatReportData({ + originalName, + custom, + }), + }); + return false; + } + + /** + * @returns true if the name is valid according to the `format` option, false otherwise + */ + function validatePredefinedFormat( + config: NormalizedSelector, + name: string, + node: TSESTree.Identifier | TSESTree.Literal, + originalName: string, + ): boolean { + const formats = config.format; + if (formats.length === 0) { + return true; + } + + for (const format of formats) { + const checker = PredefinedFormatToCheckFunction[format]; + if (checker(name)) { + return true; + } + } + + context.report({ + node, + messageId: 'doesNotMatchFormat', + data: formatReportData({ + originalName, + formats, + }), + }); + return false; + } +} + +// #region Predefined Format Functions + +/* +These format functions are taken from `tslint-consistent-codestyle/naming-convention`: +https://github.com/ajafff/tslint-consistent-codestyle/blob/ab156cc8881bcc401236d999f4ce034b59039e81/rules/namingConventionRule.ts#L603-L645 + +The licence for the code can be viewed here: +https://github.com/ajafff/tslint-consistent-codestyle/blob/ab156cc8881bcc401236d999f4ce034b59039e81/LICENSE +*/ + +/* +Why not regex here? Because it's actually really, really difficult to create a regex to handle +all of the unicode cases, and we have many non-english users that use non-english characters. +https://gist.github.com/mathiasbynens/6334847 +*/ + +function isPascalCase(name: string): boolean { + return ( + name.length === 0 || + // eslint-disable-next-line @typescript-eslint/prefer-string-starts-ends-with + (name[0] === name[0].toUpperCase() && !name.includes('_')) + ); +} +function isStrictPascalCase(name: string): boolean { + return ( + name.length === 0 || + // eslint-disable-next-line @typescript-eslint/prefer-string-starts-ends-with + (name[0] === name[0].toUpperCase() && hasStrictCamelHumps(name, true)) + ); +} + +function isCamelCase(name: string): boolean { + return ( + name.length === 0 || + // eslint-disable-next-line @typescript-eslint/prefer-string-starts-ends-with + (name[0] === name[0].toLowerCase() && !name.includes('_')) + ); +} +function isStrictCamelCase(name: string): boolean { + return ( + name.length === 0 || + // eslint-disable-next-line @typescript-eslint/prefer-string-starts-ends-with + (name[0] === name[0].toLowerCase() && hasStrictCamelHumps(name, false)) + ); +} + +function hasStrictCamelHumps(name: string, isUpper: boolean): boolean { + function isUppercaseChar(char: string): boolean { + return char === char.toUpperCase() && char !== char.toLowerCase(); + } + + if (name.startsWith('_')) { + return false; + } + for (let i = 1; i < name.length; ++i) { + if (name[i] === '_') { + return false; + } + if (isUpper === isUppercaseChar(name[i])) { + if (isUpper) { + return false; + } + } else { + isUpper = !isUpper; + } + } + return true; +} + +function isSnakeCase(name: string): boolean { + return ( + name.length === 0 || + (name === name.toLowerCase() && validateUnderscores(name)) + ); +} + +function isUpperCase(name: string): boolean { + return ( + name.length === 0 || + (name === name.toUpperCase() && validateUnderscores(name)) + ); +} + +/** Check for leading trailing and adjacent underscores */ +function validateUnderscores(name: string): boolean { + if (name.startsWith('_')) { + return false; + } + let wasUnderscore = false; + for (let i = 1; i < name.length; ++i) { + if (name[i] === '_') { + if (wasUnderscore) { + return false; + } + wasUnderscore = true; + } else { + wasUnderscore = false; + } + } + return !wasUnderscore; +} + +const PredefinedFormatToCheckFunction: Readonly boolean +>> = { + [PredefinedFormats.PascalCase]: isPascalCase, + [PredefinedFormats.StrictPascalCase]: isStrictPascalCase, + [PredefinedFormats.camelCase]: isCamelCase, + [PredefinedFormats.strictCamelCase]: isStrictCamelCase, + [PredefinedFormats.UPPER_CASE]: isUpperCase, + [PredefinedFormats.snake_case]: isSnakeCase, +}; + +// #endregion Predefined Format Functions + +function selectorTypeToMessageString(selectorType: SelectorsString): string { + const notCamelCase = selectorType.replace(/([A-Z])/g, ' $1'); + return notCamelCase.charAt(0).toUpperCase() + notCamelCase.slice(1); +} + +function isMetaSelector( + selector: IndividualAndMetaSelectorsString | Selectors | MetaSelectors, +): selector is MetaSelectorsString { + return selector in MetaSelectors; +} +function normalizeOption(option: Selector): NormalizedSelector { + let weight = 0; + option.modifiers?.forEach(mod => { + weight |= Modifiers[mod]; + }); + option.types?.forEach(mod => { + weight |= TypeModifiers[mod]; + }); + + // give selectors with a filter the _highest_ priority + if (option.filter) { + weight |= 1 << 30; + } + + return { + // format options + format: option.format.map(f => PredefinedFormats[f]), + custom: option.custom + ? { + regex: new RegExp(option.custom.regex), + match: option.custom.match, + } + : null, + leadingUnderscore: + option.leadingUnderscore !== undefined + ? UnderscoreOptions[option.leadingUnderscore] + : null, + trailingUnderscore: + option.trailingUnderscore !== undefined + ? UnderscoreOptions[option.trailingUnderscore] + : null, + prefix: option.prefix ?? null, + suffix: option.suffix ?? null, + // selector options + selector: isMetaSelector(option.selector) + ? MetaSelectors[option.selector] + : Selectors[option.selector], + modifiers: option.modifiers?.map(m => Modifiers[m]) ?? null, + types: option.types?.map(m => TypeModifiers[m]) ?? null, + filter: option.filter !== undefined ? new RegExp(option.filter) : null, + // calculated ordering weight based on modifiers + modifierWeight: weight, + }; +} + +function isCorrectType( + node: TSESTree.Node, + config: NormalizedSelector, + context: Context, +): boolean { + if (config.types === null) { + return true; + } + + const { esTreeNodeToTSNodeMap, program } = util.getParserServices(context); + const checker = program.getTypeChecker(); + const tsNode = esTreeNodeToTSNodeMap.get(node); + const type = checker + .getTypeAtLocation(tsNode) + // remove null and undefined from the type, as we don't care about it here + .getNonNullableType(); + + for (const allowedType of config.types) { + switch (allowedType) { + case TypeModifiers.array: + if ( + isAllTypesMatch( + type, + t => checker.isArrayType(t) || checker.isTupleType(t), + ) + ) { + return true; + } + break; + + case TypeModifiers.function: + if (isAllTypesMatch(type, t => t.getCallSignatures().length > 0)) { + return true; + } + break; + + case TypeModifiers.boolean: + case TypeModifiers.number: + case TypeModifiers.string: { + const typeString = checker.typeToString( + // this will resolve things like true => boolean, 'a' => string and 1 => number + checker.getWidenedType(checker.getBaseTypeOfLiteralType(type)), + ); + const allowedTypeString = TypeModifiers[allowedType]; + if (typeString === allowedTypeString) { + return true; + } + break; + } + } + } + + return false; +} + +/** + * @returns `true` if the type (or all union types) in the given type return true for the callback + */ +function isAllTypesMatch( + type: ts.Type, + cb: (type: ts.Type) => boolean, +): boolean { + if (type.isUnion()) { + return type.types.every(t => cb(t)); + } + + return cb(type); +} + +export { + MessageIds, + Options, + PredefinedFormatsString, + Selector, + selectorTypeToMessageString, +}; diff --git a/packages/eslint-plugin/src/rules/no-floating-promises.ts b/packages/eslint-plugin/src/rules/no-floating-promises.ts index 0cf9542e88a9..fdc93cbed3fe 100644 --- a/packages/eslint-plugin/src/rules/no-floating-promises.ts +++ b/packages/eslint-plugin/src/rules/no-floating-promises.ts @@ -44,9 +44,7 @@ export default util.createRule({ return { ExpressionStatement(node): void { - const { expression } = parserServices.esTreeNodeToTSNodeMap.get< - ts.ExpressionStatement - >(node); + const { expression } = parserServices.esTreeNodeToTSNodeMap.get(node); if (isUnhandledPromise(checker, expression)) { context.report({ diff --git a/packages/eslint-plugin/src/rules/no-for-in-array.ts b/packages/eslint-plugin/src/rules/no-for-in-array.ts index b93665069c71..511b744ee4c0 100644 --- a/packages/eslint-plugin/src/rules/no-for-in-array.ts +++ b/packages/eslint-plugin/src/rules/no-for-in-array.ts @@ -23,9 +23,7 @@ export default util.createRule({ ForInStatement(node): void { const parserServices = util.getParserServices(context); const checker = parserServices.program.getTypeChecker(); - const originalNode = parserServices.esTreeNodeToTSNodeMap.get< - ts.ForInStatement - >(node); + const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node); const type = checker.getTypeAtLocation(originalNode.expression); diff --git a/packages/eslint-plugin/src/rules/no-magic-numbers.ts b/packages/eslint-plugin/src/rules/no-magic-numbers.ts index b4791614cda2..6576ad4b49f8 100644 --- a/packages/eslint-plugin/src/rules/no-magic-numbers.ts +++ b/packages/eslint-plugin/src/rules/no-magic-numbers.ts @@ -55,130 +55,6 @@ export default util.createRule({ create(context, [options]) { const rules = baseRule.create(context); - /** - * Returns whether the node is number literal - * @param node the node literal being evaluated - * @returns true if the node is a number literal - */ - function isNumber(node: TSESTree.Literal): boolean { - return typeof node.value === 'number'; - } - - /** - * Checks if the node grandparent is a Typescript type alias declaration - * @param node the node to be validated. - * @returns true if the node grandparent is a Typescript type alias declaration - * @private - */ - function isGrandparentTSTypeAliasDeclaration(node: TSESTree.Node): boolean { - return node.parent && node.parent.parent - ? node.parent.parent.type === AST_NODE_TYPES.TSTypeAliasDeclaration - : false; - } - - /** - * Checks if the node grandparent is a Typescript union type and its parent is a type alias declaration - * @param node the node to be validated. - * @returns true if the node grandparent is a Typescript union type and its parent is a type alias declaration - * @private - */ - function isGrandparentTSUnionType(node: TSESTree.Node): boolean { - if ( - node.parent && - node.parent.parent && - node.parent.parent.type === AST_NODE_TYPES.TSUnionType - ) { - return isGrandparentTSTypeAliasDeclaration(node.parent); - } - - return false; - } - - /** - * Checks if the node parent is a Typescript enum member - * @param node the node to be validated. - * @returns true if the node parent is a Typescript enum member - * @private - */ - function isParentTSEnumDeclaration(node: TSESTree.Node): boolean { - return ( - typeof node.parent !== 'undefined' && - node.parent.type === AST_NODE_TYPES.TSEnumMember - ); - } - - /** - * Checks if the node parent is a Typescript literal type - * @param node the node to be validated. - * @returns true if the node parent is a Typescript literal type - * @private - */ - function isParentTSLiteralType(node: TSESTree.Node): boolean { - return node.parent - ? node.parent.type === AST_NODE_TYPES.TSLiteralType - : false; - } - - /** - * Checks if the node is a valid TypeScript numeric literal type. - * @param node the node to be validated. - * @returns true if the node is a TypeScript numeric literal type. - * @private - */ - function isTSNumericLiteralType(node: TSESTree.Node): boolean { - // For negative numbers, update the parent node - if ( - node.parent && - node.parent.type === AST_NODE_TYPES.UnaryExpression && - node.parent.operator === '-' - ) { - node = node.parent; - } - - // If the parent node is not a TSLiteralType, early return - if (!isParentTSLiteralType(node)) { - return false; - } - - // If the grandparent is a TSTypeAliasDeclaration, ignore - if (isGrandparentTSTypeAliasDeclaration(node)) { - return true; - } - - // If the grandparent is a TSUnionType and it's parent is a TSTypeAliasDeclaration, ignore - if (isGrandparentTSUnionType(node)) { - return true; - } - - return false; - } - - /** - * Checks if the node parent is a readonly class property - * @param node the node to be validated. - * @returns true if the node parent is a readonly class property - * @private - */ - function isParentTSReadonlyClassProperty(node: TSESTree.Node): boolean { - if ( - node.parent && - node.parent.type === AST_NODE_TYPES.UnaryExpression && - ['-', '+'].includes(node.parent.operator) - ) { - node = node.parent; - } - - if ( - node.parent && - node.parent.type === AST_NODE_TYPES.ClassProperty && - node.parent.readonly - ) { - return true; - } - - return false; - } - return { Literal(node): void { // Check if the node is a TypeScript enum declaration @@ -189,14 +65,17 @@ export default util.createRule({ // Check TypeScript specific nodes for Numeric Literal if ( options.ignoreNumericLiteralTypes && - isNumber(node) && + typeof node.value === 'number' && isTSNumericLiteralType(node) ) { return; } // Check if the node is a readonly class property - if (isNumber(node) && isParentTSReadonlyClassProperty(node)) { + if ( + typeof node.value === 'number' && + isParentTSReadonlyClassProperty(node) + ) { if (options.ignoreReadonlyClassProperties) { return; } @@ -207,8 +86,10 @@ export default util.createRule({ let raw = node.raw; if ( - node.parent && - node.parent.type === AST_NODE_TYPES.UnaryExpression + node.parent?.type === AST_NODE_TYPES.UnaryExpression && + // the base rule only shows the operator for negative numbers + // https://github.com/eslint/eslint/blob/9dfc8501fb1956c90dc11e6377b4cb38a6bea65d/lib/rules/no-magic-numbers.js#L126 + node.parent.operator === '-' ) { fullNumberNode = node.parent; raw = `${node.parent.operator}${node.raw}`; @@ -229,3 +110,111 @@ export default util.createRule({ }; }, }); + +/** + * Gets the true parent of the literal, handling prefixed numbers (-1 / +1) + */ +function getLiteralParent(node: TSESTree.Literal): TSESTree.Node | undefined { + if ( + node.parent?.type === AST_NODE_TYPES.UnaryExpression && + ['-', '+'].includes(node.parent.operator) + ) { + return node.parent.parent; + } + + return node.parent; +} + +/** + * Checks if the node grandparent is a Typescript type alias declaration + * @param node the node to be validated. + * @returns true if the node grandparent is a Typescript type alias declaration + * @private + */ +function isGrandparentTSTypeAliasDeclaration(node: TSESTree.Node): boolean { + return node.parent?.parent?.type === AST_NODE_TYPES.TSTypeAliasDeclaration; +} + +/** + * Checks if the node grandparent is a Typescript union type and its parent is a type alias declaration + * @param node the node to be validated. + * @returns true if the node grandparent is a Typescript union type and its parent is a type alias declaration + * @private + */ +function isGrandparentTSUnionType(node: TSESTree.Node): boolean { + if (node.parent?.parent?.type === AST_NODE_TYPES.TSUnionType) { + return isGrandparentTSTypeAliasDeclaration(node.parent); + } + + return false; +} + +/** + * Checks if the node parent is a Typescript enum member + * @param node the node to be validated. + * @returns true if the node parent is a Typescript enum member + * @private + */ +function isParentTSEnumDeclaration(node: TSESTree.Literal): boolean { + const parent = getLiteralParent(node); + return parent?.type === AST_NODE_TYPES.TSEnumMember; +} + +/** + * Checks if the node parent is a Typescript literal type + * @param node the node to be validated. + * @returns true if the node parent is a Typescript literal type + * @private + */ +function isParentTSLiteralType(node: TSESTree.Node): boolean { + return node.parent?.type === AST_NODE_TYPES.TSLiteralType; +} + +/** + * Checks if the node is a valid TypeScript numeric literal type. + * @param node the node to be validated. + * @returns true if the node is a TypeScript numeric literal type. + * @private + */ +function isTSNumericLiteralType(node: TSESTree.Node): boolean { + // For negative numbers, use the parent node + if ( + node.parent?.type === AST_NODE_TYPES.UnaryExpression && + node.parent.operator === '-' + ) { + node = node.parent; + } + + // If the parent node is not a TSLiteralType, early return + if (!isParentTSLiteralType(node)) { + return false; + } + + // If the grandparent is a TSTypeAliasDeclaration, ignore + if (isGrandparentTSTypeAliasDeclaration(node)) { + return true; + } + + // If the grandparent is a TSUnionType and it's parent is a TSTypeAliasDeclaration, ignore + if (isGrandparentTSUnionType(node)) { + return true; + } + + return false; +} + +/** + * Checks if the node parent is a readonly class property + * @param node the node to be validated. + * @returns true if the node parent is a readonly class property + * @private + */ +function isParentTSReadonlyClassProperty(node: TSESTree.Literal): boolean { + const parent = getLiteralParent(node); + + if (parent?.type === AST_NODE_TYPES.ClassProperty && parent.readonly) { + return true; + } + + return false; +} diff --git a/packages/eslint-plugin/src/rules/no-misused-promises.ts b/packages/eslint-plugin/src/rules/no-misused-promises.ts index 5326129780cf..bd74721c7a24 100644 --- a/packages/eslint-plugin/src/rules/no-misused-promises.ts +++ b/packages/eslint-plugin/src/rules/no-misused-promises.ts @@ -99,9 +99,7 @@ export default util.createRule({ | TSESTree.OptionalCallExpression | TSESTree.NewExpression, ): void { - const tsNode = parserServices.esTreeNodeToTSNodeMap.get< - ts.CallExpression | ts.NewExpression - >(node); + const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node); const voidParams = voidFunctionParams(checker, tsNode); if (voidParams.size === 0) { return; diff --git a/packages/eslint-plugin/src/rules/no-throw-literal.ts b/packages/eslint-plugin/src/rules/no-throw-literal.ts index 5d6fa1d7677e..c8c5bcc1837f 100644 --- a/packages/eslint-plugin/src/rules/no-throw-literal.ts +++ b/packages/eslint-plugin/src/rules/no-throw-literal.ts @@ -29,7 +29,11 @@ export default util.createRule({ function isErrorLike(type: ts.Type): boolean { const symbol = type.getSymbol(); - if (symbol?.getName() === 'Error') { + if (!symbol) { + return false; + } + + if (symbol.getName() === 'Error') { const declarations = symbol.getDeclarations() ?? []; for (const declaration of declarations) { const sourceFile = declaration.getSourceFile(); @@ -39,7 +43,7 @@ export default util.createRule({ } } - const baseTypes = type.getBaseTypes() ?? []; + const baseTypes = checker.getBaseTypes(type as ts.InterfaceType); for (const baseType of baseTypes) { if (isErrorLike(baseType)) { return true; diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts index e7f8e0306798..22a9fed5ba65 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts @@ -10,6 +10,7 @@ import { isFalsyType, isBooleanLiteralType, isLiteralType, + getCallSignaturesOfType, } from 'tsutils'; import { createRule, @@ -60,12 +61,15 @@ export type Options = [ { allowConstantLoopConditions?: boolean; ignoreRhs?: boolean; + checkArrayPredicates?: boolean; }, ]; export type MessageId = | 'alwaysTruthy' | 'alwaysFalsy' + | 'alwaysTruthyFunc' + | 'alwaysFalsyFunc' | 'neverNullish' | 'alwaysNullish' | 'literalBooleanExpression' @@ -92,6 +96,9 @@ export default createRule({ ignoreRhs: { type: 'boolean', }, + checkArrayPredicates: { + type: 'boolean', + }, }, additionalProperties: false, }, @@ -100,6 +107,10 @@ export default createRule({ messages: { alwaysTruthy: 'Unnecessary conditional, value is always truthy.', alwaysFalsy: 'Unnecessary conditional, value is always falsy.', + alwaysTruthyFunc: + 'This callback should return a conditional, but return is always truthy', + alwaysFalsyFunc: + 'This callback should return a conditional, but return is always falsy', neverNullish: 'Unnecessary conditional, expected left-hand side of `??` operator to be possibly null or undefined.', alwaysNullish: @@ -114,23 +125,32 @@ export default createRule({ { allowConstantLoopConditions: false, ignoreRhs: false, + checkArrayPredicates: false, }, ], - create(context, [{ allowConstantLoopConditions, ignoreRhs }]) { + create( + context, + [{ allowConstantLoopConditions, checkArrayPredicates, ignoreRhs }], + ) { const service = getParserServices(context); const checker = service.program.getTypeChecker(); const sourceCode = context.getSourceCode(); - function getNodeType(node: TSESTree.Node): ts.Type { + function getNodeType(node: TSESTree.Expression): ts.Type { const tsNode = service.esTreeNodeToTSNodeMap.get(node); return getConstrainedTypeAtLocation(checker, tsNode); } + function nodeIsArrayType(node: TSESTree.Expression): boolean { + const nodeType = getNodeType(node); + return checker.isArrayType(nodeType) || checker.isTupleType(nodeType); + } + /** * Checks if a conditional node is necessary: * if the type of the node is always true or always false, it's not necessary. */ - function checkNode(node: TSESTree.Node): void { + function checkNode(node: TSESTree.Expression): void { const type = getNodeType(node); // Conditional is always necessary if it involves: @@ -160,7 +180,7 @@ export default createRule({ } } - function checkNodeForNullish(node: TSESTree.Node): void { + function checkNodeForNullish(node: TSESTree.Expression): void { const type = getNodeType(node); // Conditional is always necessary if it involves `any` or `unknown` if (isTypeFlagSet(type, ts.TypeFlags.Any | ts.TypeFlags.Unknown)) { @@ -270,6 +290,77 @@ export default createRule({ checkNode(node.test); } + const ARRAY_PREDICATE_FUNCTIONS = new Set([ + 'filter', + 'find', + 'some', + 'every', + ]); + function shouldCheckCallback(node: TSESTree.CallExpression): boolean { + const { callee } = node; + return ( + // option is on + !!checkArrayPredicates && + // looks like `something.filter` or `something.find` + callee.type === AST_NODE_TYPES.MemberExpression && + callee.property.type === AST_NODE_TYPES.Identifier && + ARRAY_PREDICATE_FUNCTIONS.has(callee.property.name) && + // and the left-hand side is an array, according to the types + nodeIsArrayType(callee.object) + ); + } + function checkCallExpression(node: TSESTree.CallExpression): void { + const { + arguments: [callback], + } = node; + if (callback && shouldCheckCallback(node)) { + // Inline defined functions + if ( + (callback.type === AST_NODE_TYPES.ArrowFunctionExpression || + callback.type === AST_NODE_TYPES.FunctionExpression) && + callback.body + ) { + // Two special cases, where we can directly check the node that's returned: + // () => something + if (callback.body.type !== AST_NODE_TYPES.BlockStatement) { + return checkNode(callback.body); + } + // () => { return something; } + const callbackBody = callback.body.body; + if ( + callbackBody.length === 1 && + callbackBody[0].type === AST_NODE_TYPES.ReturnStatement && + callbackBody[0].argument + ) { + return checkNode(callbackBody[0].argument); + } + // Potential enhancement: could use code-path analysis to check + // any function with a single return statement + // (Value to complexity ratio is dubious however) + } + // Otherwise just do type analysis on the function as a whole. + const returnTypes = getCallSignaturesOfType( + getNodeType(callback), + ).map(sig => sig.getReturnType()); + /* istanbul ignore if */ if (returnTypes.length === 0) { + // Not a callable function + return; + } + if (!returnTypes.some(isPossiblyFalsy)) { + return context.report({ + node: callback, + messageId: 'alwaysTruthyFunc', + }); + } + if (!returnTypes.some(isPossiblyTruthy)) { + return context.report({ + node: callback, + messageId: 'alwaysFalsyFunc', + }); + } + } + } + function checkOptionalChain( node: TSESTree.OptionalMemberExpression | TSESTree.OptionalCallExpression, beforeOperator: TSESTree.Node, @@ -323,6 +414,7 @@ export default createRule({ return { BinaryExpression: checkIfBinaryExpressionIsNecessaryConditional, + CallExpression: checkCallExpression, ConditionalExpression: checkIfTestExpressionIsNecessaryConditional, DoWhileStatement: checkIfLoopIsNecessaryConditional, ForStatement: checkIfLoopIsNecessaryConditional, diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-qualifier.ts b/packages/eslint-plugin/src/rules/no-unnecessary-qualifier.ts index f489fd3c3318..a34533d2fc25 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-qualifier.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-qualifier.ts @@ -74,7 +74,7 @@ export default util.createRule({ } function qualifierIsUnnecessary( - qualifier: TSESTree.Node, + qualifier: TSESTree.EntityName | TSESTree.MemberExpression, name: TSESTree.Identifier, ): boolean { const tsQualifier = esTreeNodeToTSNodeMap.get(qualifier); @@ -110,7 +110,7 @@ export default util.createRule({ function visitNamespaceAccess( node: TSESTree.Node, - qualifier: TSESTree.Node, + qualifier: TSESTree.EntityName | TSESTree.MemberExpression, name: TSESTree.Identifier, ): void { // Only look for nested qualifier errors if we didn't already fail on the outer qualifier. @@ -132,7 +132,12 @@ export default util.createRule({ } } - function enterDeclaration(node: TSESTree.Node): void { + function enterDeclaration( + node: + | TSESTree.TSModuleDeclaration + | TSESTree.TSEnumDeclaration + | TSESTree.ExportNamedDeclaration, + ): void { namespacesInScope.push(esTreeNodeToTSNodeMap.get(node)); } @@ -152,7 +157,9 @@ export default util.createRule({ return node.type === AST_NODE_TYPES.MemberExpression && !node.computed; } - function isEntityNameExpression(node: TSESTree.Node): boolean { + function isEntityNameExpression( + node: TSESTree.Node, + ): node is TSESTree.Identifier | TSESTree.MemberExpression { return ( node.type === AST_NODE_TYPES.Identifier || (isPropertyAccessExpression(node) && diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-type-arguments.ts b/packages/eslint-plugin/src/rules/no-unnecessary-type-arguments.ts index a32cb94728c2..e900a0b07072 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-type-arguments.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-type-arguments.ts @@ -5,10 +5,14 @@ import * as util from '../util'; import { findFirstResult } from '../util'; type ParameterCapableTSNode = + | ts.TaggedTemplateExpression + | ts.ImportTypeNode | ts.CallExpression | ts.NewExpression | ts.TypeReferenceNode - | ts.ExpressionWithTypeArguments; + | ts.ExpressionWithTypeArguments + | ts.JsxOpeningElement + | ts.JsxSelfClosingElement; type MessageIds = 'unnecessaryTypeParameter'; @@ -67,9 +71,7 @@ export default util.createRule<[], MessageIds>({ return { TSTypeParameterInstantiation(node): void { - const expression = parserServices.esTreeNodeToTSNodeMap.get< - ParameterCapableTSNode - >(node); + const expression = parserServices.esTreeNodeToTSNodeMap.get(node); const typeParameters = getTypeParametersFromNode(expression, checker); if (typeParameters) { diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-type-assertion.ts b/packages/eslint-plugin/src/rules/no-unnecessary-type-assertion.ts index 6696b127c10b..943beb459d7e 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-type-assertion.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-type-assertion.ts @@ -1,6 +1,7 @@ import { TSESTree } from '@typescript-eslint/experimental-utils'; import { isCallExpression, + isJsxExpression, isNewExpression, isObjectType, isObjectFlagSet, @@ -117,6 +118,8 @@ export default util.createRule({ return parent.type ? checker.getTypeFromTypeNode(parent.type) : undefined; + } else if (isJsxExpression(parent)) { + return checker.getContextualType(parent); } else if ( ![ts.SyntaxKind.TemplateSpan, ts.SyntaxKind.JsxExpression].includes( parent.kind, @@ -168,9 +171,7 @@ export default util.createRule({ return { TSNonNullExpression(node): void { - const originalNode = parserServices.esTreeNodeToTSNodeMap.get< - ts.NonNullExpression - >(node); + const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node); const type = util.getConstrainedTypeAtLocation( checker, originalNode.expression, @@ -252,9 +253,7 @@ export default util.createRule({ return; } - const originalNode = parserServices.esTreeNodeToTSNodeMap.get< - ts.AssertionExpression - >(node); + const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node); const castType = checker.getTypeAtLocation(originalNode); if ( diff --git a/packages/eslint-plugin/src/rules/no-unused-vars-experimental.ts b/packages/eslint-plugin/src/rules/no-unused-vars-experimental.ts index db21c42570c1..0c1ac212380a 100644 --- a/packages/eslint-plugin/src/rules/no-unused-vars-experimental.ts +++ b/packages/eslint-plugin/src/rules/no-unused-vars-experimental.ts @@ -290,7 +290,7 @@ export default util.createRule({ } return { - 'Program:exit'(program: TSESTree.Node): void { + 'Program:exit'(program: TSESTree.Program): void { const tsNode = parserServices.esTreeNodeToTSNodeMap.get(program); const sourceFile = util.getSourceFileOfNode(tsNode); const diagnostics = tsProgram.getSemanticDiagnostics(sourceFile); diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index 805f17d4f60c..d5b35e8dd136 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -4,7 +4,6 @@ import { TSESLint, TSESTree, } from '@typescript-eslint/experimental-utils'; -import * as ts from 'typescript'; import * as util from '../util'; export type Options = [ @@ -75,9 +74,7 @@ export default util.createRule({ 'LogicalExpression[operator = "||"]'( node: TSESTree.LogicalExpression, ): void { - const tsNode = parserServices.esTreeNodeToTSNodeMap.get< - ts.BinaryExpression - >(node); + const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node); const type = checker.getTypeAtLocation(tsNode.left); const isNullish = util.isNullableType(type, { allowUndefined: true }); if (!isNullish) { diff --git a/packages/eslint-plugin/src/rules/prefer-readonly.ts b/packages/eslint-plugin/src/rules/prefer-readonly.ts index d2aa135b99f4..d51b8c0f954f 100644 --- a/packages/eslint-plugin/src/rules/prefer-readonly.ts +++ b/packages/eslint-plugin/src/rules/prefer-readonly.ts @@ -135,7 +135,9 @@ export default util.createRule({ return false; } - function isConstructor(node: TSESTree.Node): boolean { + function isConstructor( + node: TSESTree.Node, + ): node is TSESTree.MethodDefinition { return ( node.type === AST_NODE_TYPES.MethodDefinition && node.kind === 'constructor' @@ -143,7 +145,11 @@ export default util.createRule({ } function isFunctionScopeBoundaryInStack( - node: TSESTree.Node, + node: + | TSESTree.ArrowFunctionExpression + | TSESTree.FunctionDeclaration + | TSESTree.FunctionExpression + | TSESTree.MethodDefinition, ): boolean | tsutils.ScopeBoundary { if (classScopeStack.length === 0) { return false; @@ -208,10 +214,10 @@ export default util.createRule({ } }, MemberExpression(node): void { - const tsNode = parserServices.esTreeNodeToTSNodeMap.get< - ts.PropertyAccessExpression - >(node); if (classScopeStack.length !== 0 && !node.computed) { + const tsNode = parserServices.esTreeNodeToTSNodeMap.get( + node, + ) as ts.PropertyAccessExpression; handlePropertyAccessExpression( tsNode, tsNode.parent, @@ -228,9 +234,7 @@ export default util.createRule({ ): void { if (isConstructor(node)) { classScopeStack[classScopeStack.length - 1].enterConstructor( - parserServices.esTreeNodeToTSNodeMap.get( - node, - ), + parserServices.esTreeNodeToTSNodeMap.get(node), ); } else if (isFunctionScopeBoundaryInStack(node)) { classScopeStack[classScopeStack.length - 1].enterNonConstructor(); @@ -339,7 +343,13 @@ class ClassScope { ).add(node.name.text); } - public enterConstructor(node: ts.ConstructorDeclaration): void { + public enterConstructor( + node: + | ts.GetAccessorDeclaration + | ts.SetAccessorDeclaration + | ts.MethodDeclaration + | ts.ConstructorDeclaration, + ): void { this.constructorScopeDepth = DIRECTLY_INSIDE_CONSTRUCTOR; for (const parameter of node.parameters) { diff --git a/packages/eslint-plugin/src/rules/prefer-regexp-exec.ts b/packages/eslint-plugin/src/rules/prefer-regexp-exec.ts index e62033603b53..599e0f2a6da3 100644 --- a/packages/eslint-plugin/src/rules/prefer-regexp-exec.ts +++ b/packages/eslint-plugin/src/rules/prefer-regexp-exec.ts @@ -30,7 +30,7 @@ export default createRule({ * Check if a given node is a string. * @param node The node to check. */ - function isStringType(node: TSESTree.Node): boolean { + function isStringType(node: TSESTree.LeftHandSideExpression): boolean { const objectType = typeChecker.getTypeAtLocation( service.esTreeNodeToTSNodeMap.get(node), ); diff --git a/packages/eslint-plugin/src/rules/prefer-string-starts-ends-with.ts b/packages/eslint-plugin/src/rules/prefer-string-starts-ends-with.ts index d8872bdfa25d..2b4946b2a14c 100644 --- a/packages/eslint-plugin/src/rules/prefer-string-starts-ends-with.ts +++ b/packages/eslint-plugin/src/rules/prefer-string-starts-ends-with.ts @@ -45,7 +45,7 @@ export default createRule({ * Check if a given node is a string. * @param node The node to check. */ - function isStringType(node: TSESTree.Node): boolean { + function isStringType(node: TSESTree.LeftHandSideExpression): boolean { const objectType = typeChecker.getTypeAtLocation( service.esTreeNodeToTSNodeMap.get(node), ); diff --git a/packages/eslint-plugin/src/rules/promise-function-async.ts b/packages/eslint-plugin/src/rules/promise-function-async.ts index e736f5a92019..16134675d571 100644 --- a/packages/eslint-plugin/src/rules/promise-function-async.ts +++ b/packages/eslint-plugin/src/rules/promise-function-async.ts @@ -90,7 +90,14 @@ export default util.createRule({ const parserServices = util.getParserServices(context); const checker = parserServices.program.getTypeChecker(); - function validateNode(node: TSESTree.Node): void { + function validateNode( + node: + | TSESTree.ArrowFunctionExpression + | TSESTree.FunctionDeclaration + | TSESTree.FunctionExpression + | TSESTree.MethodDefinition + | TSESTree.TSAbstractMethodDefinition, + ): void { const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node); const signatures = checker .getTypeAtLocation(originalNode) diff --git a/packages/eslint-plugin/src/rules/require-array-sort-compare.ts b/packages/eslint-plugin/src/rules/require-array-sort-compare.ts index 580b36f591d6..25c4c2e57475 100644 --- a/packages/eslint-plugin/src/rules/require-array-sort-compare.ts +++ b/packages/eslint-plugin/src/rules/require-array-sort-compare.ts @@ -9,7 +9,8 @@ export default util.createRule({ meta: { type: 'problem', docs: { - description: 'Enforce giving `compare` argument to `Array#sort`', + description: + 'Requires `Array#sort` calls to always provide a `compareFunction`', category: 'Best Practices', recommended: false, requiresTypeChecking: true, diff --git a/packages/eslint-plugin/src/rules/require-await.ts b/packages/eslint-plugin/src/rules/require-await.ts index ed28a105430c..b9949659daaf 100644 --- a/packages/eslint-plugin/src/rules/require-await.ts +++ b/packages/eslint-plugin/src/rules/require-await.ts @@ -66,9 +66,7 @@ export default util.createRule({ ForOfStatement: rules.ForOfStatement, ReturnStatement(node): void { - const { expression } = parserServices.esTreeNodeToTSNodeMap.get< - ts.ReturnStatement - >(node); + const { expression } = parserServices.esTreeNodeToTSNodeMap.get(node); if (expression && isThenableType(expression)) { // tell the base rule to mark the scope as having an await so it ignores it rules.AwaitExpression(); diff --git a/packages/eslint-plugin/src/rules/restrict-plus-operands.ts b/packages/eslint-plugin/src/rules/restrict-plus-operands.ts index f07e624eeb23..a833825a207c 100644 --- a/packages/eslint-plugin/src/rules/restrict-plus-operands.ts +++ b/packages/eslint-plugin/src/rules/restrict-plus-operands.ts @@ -96,7 +96,7 @@ export default util.createRule({ * Helper function to get base type of node * @param node the node to be evaluated. */ - function getNodeType(node: TSESTree.Node): BaseLiteral { + function getNodeType(node: TSESTree.Expression): BaseLiteral { const tsNode = service.esTreeNodeToTSNodeMap.get(node); const type = typeChecker.getTypeAtLocation(tsNode); diff --git a/packages/eslint-plugin/src/rules/restrict-template-expressions.ts b/packages/eslint-plugin/src/rules/restrict-template-expressions.ts index efedc786b512..6ff1627daf50 100644 --- a/packages/eslint-plugin/src/rules/restrict-template-expressions.ts +++ b/packages/eslint-plugin/src/rules/restrict-template-expressions.ts @@ -92,7 +92,7 @@ export default util.createRule({ * Helper function to get base type of node * @param node the node to be evaluated. */ - function getNodeType(node: TSESTree.Node): BaseType[] { + function getNodeType(node: TSESTree.Expression): BaseType[] { const tsNode = service.esTreeNodeToTSNodeMap.get(node); const type = typeChecker.getTypeAtLocation(tsNode); diff --git a/packages/eslint-plugin/src/rules/return-await.ts b/packages/eslint-plugin/src/rules/return-await.ts index 43d9b3e0f55e..52ffb052c7e8 100644 --- a/packages/eslint-plugin/src/rules/return-await.ts +++ b/packages/eslint-plugin/src/rules/return-await.ts @@ -135,9 +135,7 @@ export default util.createRule({ } }, ReturnStatement(node): void { - const originalNode = parserServices.esTreeNodeToTSNodeMap.get< - ts.ReturnStatement - >(node); + const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node); const { expression } = originalNode; diff --git a/packages/eslint-plugin/src/rules/strict-boolean-expressions.ts b/packages/eslint-plugin/src/rules/strict-boolean-expressions.ts index 0d4235e2d7d4..c924045ba028 100644 --- a/packages/eslint-plugin/src/rules/strict-boolean-expressions.ts +++ b/packages/eslint-plugin/src/rules/strict-boolean-expressions.ts @@ -66,10 +66,8 @@ export default util.createRule({ /** * Determines if the node is safe for boolean type */ - function isValidBooleanNode(node: TSESTree.Node): boolean { - const tsNode = service.esTreeNodeToTSNodeMap.get( - node, - ); + function isValidBooleanNode(node: TSESTree.Expression): boolean { + const tsNode = service.esTreeNodeToTSNodeMap.get(node); const type = util.getConstrainedTypeAtLocation(checker, tsNode); if (tsutils.isTypeFlagSet(type, ts.TypeFlags.BooleanLike)) { diff --git a/packages/eslint-plugin/src/util/misc.ts b/packages/eslint-plugin/src/util/misc.ts index 10a12a950565..c024b6a845cc 100644 --- a/packages/eslint-plugin/src/util/misc.ts +++ b/packages/eslint-plugin/src/util/misc.ts @@ -126,11 +126,16 @@ type RequireKeys< TKeys extends keyof TObj > = ExcludeKeys & { [k in TKeys]-?: Exclude }; +function getEnumNames(myEnum: Record): T[] { + return Object.keys(myEnum).filter(x => isNaN(parseInt(x))) as T[]; +} + export { arraysAreEqual, Equal, ExcludeKeys, findFirstResult, + getEnumNames, getNameFromMember, InferMessageIdsTypeFromRule, InferOptionsTypeFromRule, diff --git a/packages/eslint-plugin/tests/RuleTester.ts b/packages/eslint-plugin/tests/RuleTester.ts index 7d8bdb2c6955..c650c83449c4 100644 --- a/packages/eslint-plugin/tests/RuleTester.ts +++ b/packages/eslint-plugin/tests/RuleTester.ts @@ -44,6 +44,7 @@ class RuleTester extends TSESLint.RuleTester { ): void { const errorMessage = `Do not set the parser at the test level unless you want to use a parser other than ${parser}`; + // standardize the valid tests as objects tests.valid = tests.valid.map(test => { if (typeof test === 'string') { return { diff --git a/packages/eslint-plugin/tests/configs.test.ts b/packages/eslint-plugin/tests/configs.test.ts new file mode 100644 index 000000000000..eea9079f8ba3 --- /dev/null +++ b/packages/eslint-plugin/tests/configs.test.ts @@ -0,0 +1,69 @@ +import rules from '../src/rules'; +import plugin from '../src/index'; + +function entriesToObject(value: [string, T][]): Record { + return value.reduce>((accum, [k, v]) => { + accum[k] = v; + return accum; + }, {}); +} + +function filterRules(values: Record): [string, string][] { + return Object.entries(values).filter(([name]) => + name.startsWith(RULE_NAME_PREFIX), + ); +} + +const RULE_NAME_PREFIX = '@typescript-eslint/'; + +describe('all.json config', () => { + const configRules = filterRules(plugin.configs.all.rules); + // note: exclude deprecated rules, this config is allowed to change between minor versions + const ruleConfigs = Object.entries(rules) + .filter(([, rule]) => !rule.meta.deprecated) + .map<[string, string]>(([name]) => [`${RULE_NAME_PREFIX}${name}`, 'error']); + + it('contains all of the rules, excluding the deprecated ones', () => { + expect(entriesToObject(ruleConfigs)).toEqual(entriesToObject(configRules)); + }); +}); + +describe('recommended.json config', () => { + const configRules = filterRules(plugin.configs.recommended.rules); + // note: include deprecated rules so that the config doesn't change between major bumps + const ruleConfigs = Object.entries(rules) + .filter( + ([, rule]) => + rule.meta.docs.recommended !== false && + rule.meta.docs.requiresTypeChecking !== true, + ) + .map<[string, string]>(([name, rule]) => [ + `${RULE_NAME_PREFIX}${name}`, + rule.meta.docs.recommended || 'off', + ]); + + it("contains all recommended rules that don't require typechecking, excluding the deprecated ones", () => { + expect(entriesToObject(ruleConfigs)).toEqual(entriesToObject(configRules)); + }); +}); + +describe('recommended-requiring-type-checking.json config', () => { + const configRules = filterRules( + plugin.configs['recommended-requiring-type-checking'].rules, + ); + // note: include deprecated rules so that the config doesn't change between major bumps + const ruleConfigs = Object.entries(rules) + .filter( + ([, rule]) => + rule.meta.docs.recommended !== false && + rule.meta.docs.requiresTypeChecking === true, + ) + .map<[string, string]>(([name, rule]) => [ + `${RULE_NAME_PREFIX}${name}`, + rule.meta.docs.recommended || 'off', + ]); + + it('contains all recommended rules that require type checking, excluding the deprecated ones', () => { + expect(entriesToObject(ruleConfigs)).toEqual(entriesToObject(configRules)); + }); +}); diff --git a/packages/eslint-plugin/tests/configs/all.test.ts b/packages/eslint-plugin/tests/configs/all.test.ts deleted file mode 100644 index 871576f2543a..000000000000 --- a/packages/eslint-plugin/tests/configs/all.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -import rules from '../../src/rules'; -import allConfig from '../../src/configs/all.json'; - -interface JsonRules { - [name: string]: string; -} - -describe('all.json config', () => { - const RULE_NAME_PREFIX = '@typescript-eslint/'; - - const rulesNames = Object.keys(rules) as (keyof typeof rules)[]; - const notDeprecatedRuleNames = rulesNames.reduce( - (collection, name) => { - if (!rules[name].meta.deprecated) { - collection.push(`${RULE_NAME_PREFIX}${name}`); - } - return collection; - }, - [], - ); - - // with end of Node.js 6 support, we can use Object.entries(allConfig.rules) here - const configRules: JsonRules = allConfig.rules; - const typescriptEslintConfigRules = Object.keys(configRules).filter(name => - name.startsWith(RULE_NAME_PREFIX), - ); - const typescriptEslintConfigRuleValues = typescriptEslintConfigRules.map( - name => configRules[name], - ); - - it('contains all @typescript-eslint/eslint-plugin rule modules, except the deprecated ones', () => { - expect(notDeprecatedRuleNames).toEqual( - expect.arrayContaining(typescriptEslintConfigRules), - ); - }); - - it('has all containing @typescript-eslint/eslint-plugin rules enabled with "error"', () => { - expect(['error']).toEqual( - expect.arrayContaining(typescriptEslintConfigRuleValues), - ); - }); -}); diff --git a/packages/eslint-plugin/tests/docs.test.ts b/packages/eslint-plugin/tests/docs.test.ts new file mode 100644 index 000000000000..d85dc8fb204e --- /dev/null +++ b/packages/eslint-plugin/tests/docs.test.ts @@ -0,0 +1,141 @@ +import fs from 'fs'; +import path from 'path'; + +import marked from 'marked'; +import rules from '../src/rules'; + +const docsRoot = path.resolve(__dirname, '../docs/rules'); +const rulesData = Object.entries(rules); + +function createRuleLink(ruleName: string): string { + return `[\`@typescript-eslint/${ruleName}\`](./docs/rules/${ruleName}.md)`; +} + +function parseReadme(): marked.Tokens.Table { + const readmeRaw = fs.readFileSync( + path.resolve(__dirname, '../README.md'), + 'utf8', + ); + const readme = marked.lexer(readmeRaw, { + gfm: true, + silent: false, + }); + + // find the table + const rulesTable = readme.find( + (token): token is marked.Tokens.Table => token.type === 'table', + ); + if (!rulesTable) { + throw Error('Could not find the rules table in README.md'); + } + + return rulesTable; +} + +describe('Validating rule docs', () => { + it('All rules must have a corresponding rule doc', () => { + const files = fs.readdirSync(docsRoot); + const ruleFiles = Object.keys(rules) + .map(rule => `${rule}.md`) + .sort(); + + expect(files.sort()).toEqual(ruleFiles); + }); + + for (const [ruleName, rule] of rulesData) { + const filePath = path.join(docsRoot, `${ruleName}.md`); + it(`Description of ${ruleName}.md must match`, () => { + // validate if description of rule is same as in docs + const file = fs.readFileSync(filePath, 'utf-8'); + const tokens = marked.lexer(file, { + gfm: true, + silent: false, + }); + + // Rule title not found. + // Rule title does not match the rule metadata. + expect(tokens[0]).toEqual({ + type: 'heading', + depth: 1, + text: `${rule.meta.docs.description} (\`${ruleName}\`)`, + }); + }); + } +}); + +describe('Validating rule metadata', () => { + for (const [ruleName, rule] of rulesData) { + describe(`${ruleName}`, () => { + 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(); + }); + + it('`requiresTypeChecking` should be set if the rule uses type information', () => { + // quick-and-dirty check to see if it uses parserServices + // not perfect but should be good enough + const ruleFileContents = fs.readFileSync( + path.resolve(__dirname, `../src/rules/${ruleName}.ts`), + ); + + expect(ruleFileContents.includes('getParserServices')).toEqual( + rule.meta.docs.requiresTypeChecking ?? false, + ); + }); + }); + } +}); + +describe('Validating README.md', () => { + const rulesTable = parseReadme().cells; + const notDeprecated = rulesData.filter( + ([, rule]) => rule.meta.deprecated !== true, + ); + + it('All non-deprecated rules should have a row in the table, and the table should be ordered alphabetically', () => { + const ruleNames = notDeprecated + .map(([ruleName]) => ruleName) + .sort() + .map(createRuleLink); + + expect(rulesTable.map(row => row[0])).toStrictEqual(ruleNames); + }); + + for (const [ruleName, rule] of notDeprecated) { + describe(`Checking rule ${ruleName}`, () => { + const ruleRow = + rulesTable.find(row => row[0].includes(`/${ruleName}.md`)) ?? []; + + it('Link column should be correct', () => { + expect(ruleRow[0]).toEqual(createRuleLink(ruleName)); + }); + + it('Description column should be correct', () => { + expect(ruleRow[1]).toEqual(rule.meta.docs.description); + }); + + it('Recommended column should be correct', () => { + expect(ruleRow[2]).toEqual( + rule.meta.docs.recommended ? ':heavy_check_mark:' : '', + ); + }); + + it('Fixable column should be correct', () => { + expect(ruleRow[3]).toEqual( + rule.meta.fixable !== undefined ? ':wrench:' : '', + ); + }); + + it('Requiring type information column should be correct', () => { + expect(ruleRow[4]).toEqual( + rule.meta.docs.requiresTypeChecking === true + ? ':thought_balloon:' + : '', + ); + }); + }); + } +}); diff --git a/packages/eslint-plugin/tests/fixtures/react.tsx b/packages/eslint-plugin/tests/fixtures/react.tsx new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/eslint-plugin/tests/fixtures/tsconfig.json b/packages/eslint-plugin/tests/fixtures/tsconfig.json index 92694993c539..7ff53268e423 100644 --- a/packages/eslint-plugin/tests/fixtures/tsconfig.json +++ b/packages/eslint-plugin/tests/fixtures/tsconfig.json @@ -1,10 +1,15 @@ { "compilerOptions": { + "jsx": "preserve", "target": "es5", "module": "commonjs", "strict": true, "esModuleInterop": true, "lib": ["es2015", "es2017", "esnext"], "experimentalDecorators": true - } + }, + "include": [ + "file.ts", + "react.tsx" + ] } diff --git a/packages/eslint-plugin/tests/rules/default-param-last.test.ts b/packages/eslint-plugin/tests/rules/default-param-last.test.ts new file mode 100644 index 000000000000..dd70b3b05cd3 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/default-param-last.test.ts @@ -0,0 +1,531 @@ +import rule from '../../src/rules/default-param-last'; +import { RuleTester } from '../RuleTester'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', +}); + +ruleTester.run('default-param-last', rule, { + valid: [ + 'function foo() {}', + 'function foo(a: number) {}', + 'function foo(a = 1) {}', + 'function foo(a?: number) {}', + 'function foo(a: number, b: number) {}', + 'function foo(a: number, b: number, c?: number) {}', + 'function foo(a: number, b = 1) {}', + 'function foo(a: number, b = 1, c = 1) {}', + 'function foo(a: number, b = 1, c?: number) {}', + 'function foo(a: number, b?: number, c = 1) {}', + 'function foo(a: number, b = 1, ...c) {}', + + 'const foo = function () {}', + 'const foo = function (a: number) {}', + 'const foo = function (a = 1) {}', + 'const foo = function (a?: number) {}', + 'const foo = function (a: number, b: number) {}', + 'const foo = function (a: number, b: number, c?: number) {}', + 'const foo = function (a: number, b = 1) {}', + 'const foo = function (a: number, b = 1, c = 1) {}', + 'const foo = function (a: number, b = 1, c?: number) {}', + 'const foo = function (a: number, b?: number, c = 1) {}', + 'const foo = function (a: number, b = 1, ...c) {}', + + 'const foo = () => {}', + 'const foo = (a: number) => {}', + 'const foo = (a = 1) => {}', + 'const foo = (a?: number) => {}', + 'const foo = (a: number, b: number) => {}', + 'const foo = (a: number, b: number, c?: number) => {}', + 'const foo = (a: number, b = 1) => {}', + 'const foo = (a: number, b = 1, c = 1) => {}', + 'const foo = (a: number, b = 1, c?: number) => {}', + 'const foo = (a: number, b?: number, c = 1) => {}', + 'const foo = (a: number, b = 1, ...c) => {}', + ], + invalid: [ + { + code: 'function foo(a = 1, b: number) {}', + errors: [ + { + messageId: 'shouldBeLast', + line: 1, + column: 14, + endColumn: 19, + }, + ], + }, + { + code: 'function foo(a = 1, b = 2, c: number) {}', + errors: [ + { + messageId: 'shouldBeLast', + line: 1, + column: 14, + endColumn: 19, + }, + { + messageId: 'shouldBeLast', + line: 1, + column: 21, + endColumn: 26, + }, + ], + }, + { + code: 'function foo(a = 1, b: number, c = 2, d: number) {}', + errors: [ + { + messageId: 'shouldBeLast', + line: 1, + column: 14, + endColumn: 19, + }, + { + messageId: 'shouldBeLast', + line: 1, + column: 32, + endColumn: 37, + }, + ], + }, + { + code: 'function foo(a = 1, b: number, c = 2) {}', + errors: [ + { + messageId: 'shouldBeLast', + line: 1, + column: 14, + endColumn: 19, + }, + ], + }, + { + code: 'function foo(a = 1, b: number, ...c) {}', + errors: [ + { + messageId: 'shouldBeLast', + line: 1, + column: 14, + endColumn: 19, + }, + ], + }, + { + code: 'function foo(a?: number, b: number) {}', + errors: [ + { + messageId: 'shouldBeLast', + line: 1, + column: 14, + endColumn: 24, + }, + ], + }, + { + code: 'function foo(a: number, b?: number, c: number) {}', + errors: [ + { + messageId: 'shouldBeLast', + line: 1, + column: 25, + endColumn: 35, + }, + ], + }, + { + code: 'function foo(a = 1, b?: number, c: number) {}', + errors: [ + { + messageId: 'shouldBeLast', + line: 1, + column: 14, + endColumn: 19, + }, + { + messageId: 'shouldBeLast', + line: 1, + column: 21, + endColumn: 31, + }, + ], + }, + { + code: 'function foo(a = 1, { b }) {}', + errors: [ + { + messageId: 'shouldBeLast', + line: 1, + column: 14, + endColumn: 19, + }, + ], + }, + { + code: 'function foo({ a } = {}, b) {}', + errors: [ + { + messageId: 'shouldBeLast', + line: 1, + column: 14, + endColumn: 24, + }, + ], + }, + { + code: 'function foo({ a, b } = { a: 1, b: 2 }, c) {}', + errors: [ + { + messageId: 'shouldBeLast', + line: 1, + column: 14, + endColumn: 39, + }, + ], + }, + { + code: 'function foo([a] = [], b) {}', + errors: [ + { + messageId: 'shouldBeLast', + line: 1, + column: 14, + endColumn: 22, + }, + ], + }, + { + code: 'function foo([a, b] = [1, 2], c) {}', + errors: [ + { + messageId: 'shouldBeLast', + line: 1, + column: 14, + endColumn: 29, + }, + ], + }, + { + code: 'const foo = function(a = 1, b: number) {}', + errors: [ + { + messageId: 'shouldBeLast', + line: 1, + column: 22, + endColumn: 27, + }, + ], + }, + { + code: 'const foo = function(a = 1, b = 2, c: number) {}', + errors: [ + { + messageId: 'shouldBeLast', + line: 1, + column: 22, + endColumn: 27, + }, + { + messageId: 'shouldBeLast', + line: 1, + column: 29, + endColumn: 34, + }, + ], + }, + { + code: 'const foo = function(a = 1, b: number, c = 2, d: number) {}', + errors: [ + { + messageId: 'shouldBeLast', + line: 1, + column: 22, + endColumn: 27, + }, + { + messageId: 'shouldBeLast', + line: 1, + column: 40, + endColumn: 45, + }, + ], + }, + { + code: 'const foo = function(a = 1, b: number, c = 2) {}', + errors: [ + { + messageId: 'shouldBeLast', + line: 1, + column: 22, + endColumn: 27, + }, + ], + }, + { + code: 'const foo = function(a = 1, b: number, ...c) {}', + errors: [ + { + messageId: 'shouldBeLast', + line: 1, + column: 22, + endColumn: 27, + }, + ], + }, + { + code: 'const foo = function(a?: number, b: number) {}', + errors: [ + { + messageId: 'shouldBeLast', + line: 1, + column: 22, + endColumn: 32, + }, + ], + }, + { + code: 'const foo = function(a: number, b?: number, c: number) {}', + errors: [ + { + messageId: 'shouldBeLast', + line: 1, + column: 33, + endColumn: 43, + }, + ], + }, + { + code: 'const foo = function(a = 1, b?: number, c: number) {}', + errors: [ + { + messageId: 'shouldBeLast', + line: 1, + column: 22, + endColumn: 27, + }, + { + messageId: 'shouldBeLast', + line: 1, + column: 29, + endColumn: 39, + }, + ], + }, + { + code: 'const foo = function(a = 1, { b }) {}', + errors: [ + { + messageId: 'shouldBeLast', + line: 1, + column: 22, + endColumn: 27, + }, + ], + }, + { + code: 'const foo = function({ a } = {}, b) {}', + errors: [ + { + messageId: 'shouldBeLast', + line: 1, + column: 22, + endColumn: 32, + }, + ], + }, + { + code: 'const foo = function({ a, b } = { a: 1, b: 2 }, c) {}', + errors: [ + { + messageId: 'shouldBeLast', + line: 1, + column: 22, + endColumn: 47, + }, + ], + }, + { + code: 'const foo = function([a] = [], b) {}', + errors: [ + { + messageId: 'shouldBeLast', + line: 1, + column: 22, + endColumn: 30, + }, + ], + }, + { + code: 'const foo = function([a, b] = [1, 2], c) {}', + errors: [ + { + messageId: 'shouldBeLast', + line: 1, + column: 22, + endColumn: 37, + }, + ], + }, + { + code: 'const foo = (a = 1, b: number) => {}', + errors: [ + { + messageId: 'shouldBeLast', + line: 1, + column: 14, + endColumn: 19, + }, + ], + }, + { + code: 'const foo = (a = 1, b = 2, c: number) => {}', + errors: [ + { + messageId: 'shouldBeLast', + line: 1, + column: 14, + endColumn: 19, + }, + { + messageId: 'shouldBeLast', + line: 1, + column: 21, + endColumn: 26, + }, + ], + }, + { + code: 'const foo = (a = 1, b: number, c = 2, d: number) => {}', + errors: [ + { + messageId: 'shouldBeLast', + line: 1, + column: 14, + endColumn: 19, + }, + { + messageId: 'shouldBeLast', + line: 1, + column: 32, + endColumn: 37, + }, + ], + }, + { + code: 'const foo = (a = 1, b: number, c = 2) => {}', + errors: [ + { + messageId: 'shouldBeLast', + line: 1, + column: 14, + endColumn: 19, + }, + ], + }, + { + code: 'const foo = (a = 1, b: number, ...c) => {}', + errors: [ + { + messageId: 'shouldBeLast', + line: 1, + column: 14, + endColumn: 19, + }, + ], + }, + { + code: 'const foo = (a?: number, b: number) => {}', + errors: [ + { + messageId: 'shouldBeLast', + line: 1, + column: 14, + endColumn: 24, + }, + ], + }, + { + code: 'const foo = (a: number, b?: number, c: number) => {}', + errors: [ + { + messageId: 'shouldBeLast', + line: 1, + column: 25, + endColumn: 35, + }, + ], + }, + { + code: 'const foo = (a = 1, b?: number, c: number) => {}', + errors: [ + { + messageId: 'shouldBeLast', + line: 1, + column: 14, + endColumn: 19, + }, + { + messageId: 'shouldBeLast', + line: 1, + column: 21, + endColumn: 31, + }, + ], + }, + { + code: 'const foo = (a = 1, { b }) => {}', + errors: [ + { + messageId: 'shouldBeLast', + line: 1, + column: 14, + endColumn: 19, + }, + ], + }, + { + code: 'const foo = ({ a } = {}, b) => {}', + errors: [ + { + messageId: 'shouldBeLast', + line: 1, + column: 14, + endColumn: 24, + }, + ], + }, + { + code: 'const foo = ({ a, b } = { a: 1, b: 2 }, c) => {}', + errors: [ + { + messageId: 'shouldBeLast', + line: 1, + column: 14, + endColumn: 39, + }, + ], + }, + { + code: 'const foo = ([a] = [], b) => {}', + errors: [ + { + messageId: 'shouldBeLast', + line: 1, + column: 14, + endColumn: 22, + }, + ], + }, + { + code: 'const foo = ([a, b] = [1, 2], c) => {}', + errors: [ + { + messageId: 'shouldBeLast', + line: 1, + column: 14, + endColumn: 29, + }, + ], + }, + ], +}); diff --git a/packages/eslint-plugin/tests/rules/naming-convention.test.ts b/packages/eslint-plugin/tests/rules/naming-convention.test.ts new file mode 100644 index 000000000000..de4625fbeb20 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/naming-convention.test.ts @@ -0,0 +1,865 @@ +import { TSESLint } from '@typescript-eslint/experimental-utils'; +import rule, { + MessageIds, + Options, + PredefinedFormatsString, + Selector, + selectorTypeToMessageString, +} from '../../src/rules/naming-convention'; +import { RuleTester, getFixturesRootDir } from '../RuleTester'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', +}); + +// only need parserOptions for the `type` option tests +const rootDir = getFixturesRootDir(); +const parserOptions = { + tsconfigRootDir: rootDir, + project: './tsconfig.json', +}; + +const formatTestNames: Readonly +>> = { + camelCase: { + valid: ['strictCamelCase', 'lower', 'camelCaseUNSTRICT'], + invalid: ['snake_case', 'UPPER_CASE', 'UPPER', 'StrictPascalCase'], + }, + strictCamelCase: { + valid: ['strictCamelCase', 'lower'], + invalid: [ + 'snake_case', + 'UPPER_CASE', + 'UPPER', + 'StrictPascalCase', + 'camelCaseUNSTRICT', + ], + }, + PascalCase: { + valid: [ + 'StrictPascalCase', + 'Pascal', + 'I18n', + 'PascalCaseUNSTRICT', + 'UPPER', + ], + invalid: ['snake_case', 'UPPER_CASE', 'strictCamelCase'], + }, + StrictPascalCase: { + valid: ['StrictPascalCase', 'Pascal', 'I18n'], + invalid: [ + 'snake_case', + 'UPPER_CASE', + 'UPPER', + 'strictCamelCase', + 'PascalCaseUNSTRICT', + ], + }, + UPPER_CASE: { + valid: ['UPPER_CASE', 'UPPER'], + invalid: [ + 'lower', + 'snake_case', + 'SNAKE_case_UNSTRICT', + 'strictCamelCase', + 'StrictPascalCase', + ], + }, + // eslint-disable-next-line @typescript-eslint/camelcase + snake_case: { + valid: ['snake_case', 'lower'], + invalid: [ + 'UPPER_CASE', + 'SNAKE_case_UNSTRICT', + 'strictCamelCase', + 'StrictPascalCase', + ], + }, +}; + +const REPLACE_REGEX = /%/g; + +type Cases = { + code: string[]; + options: Omit; +}[]; +function createValidTestCases(cases: Cases): TSESLint.ValidTestCase[] { + const newCases: TSESLint.ValidTestCase[] = []; + + for (const test of cases) { + for (const [formatLoose, names] of Object.entries(formatTestNames)) { + const format = [formatLoose as PredefinedFormatsString]; + for (const name of names.valid) { + const createCase = ( + preparedName: string, + options: Selector, + ): TSESLint.ValidTestCase => ({ + options: [ + { + ...options, + filter: '[iI]gnored', + }, + ], + code: `// ${JSON.stringify(options)}\n${test.code + .map(code => code.replace(REPLACE_REGEX, preparedName)) + .join('\n')}`, + }); + + newCases.push( + createCase(name, { + ...test.options, + format, + }), + + // leadingUnderscore + createCase(name, { + ...test.options, + format, + leadingUnderscore: 'forbid', + }), + createCase(`_${name}`, { + ...test.options, + format, + leadingUnderscore: 'require', + }), + createCase(`_${name}`, { + ...test.options, + format, + leadingUnderscore: 'allow', + }), + createCase(name, { + ...test.options, + format, + leadingUnderscore: 'allow', + }), + + // trailingUnderscore + createCase(name, { + ...test.options, + format, + trailingUnderscore: 'forbid', + }), + createCase(`${name}_`, { + ...test.options, + format, + trailingUnderscore: 'require', + }), + createCase(`${name}_`, { + ...test.options, + format, + trailingUnderscore: 'allow', + }), + createCase(name, { + ...test.options, + format, + trailingUnderscore: 'allow', + }), + + // prefix + createCase(`MyPrefix${name}`, { + ...test.options, + format, + prefix: ['MyPrefix'], + }), + createCase(`MyPrefix2${name}`, { + ...test.options, + format, + prefix: ['MyPrefix1', 'MyPrefix2'], + }), + + // suffix + createCase(`${name}MySuffix`, { + ...test.options, + format, + suffix: ['MySuffix'], + }), + createCase(`${name}MySuffix2`, { + ...test.options, + format, + suffix: ['MySuffix1', 'MySuffix2'], + }), + ); + } + } + } + + return newCases; +} +function createInvalidTestCases( + cases: Cases, +): TSESLint.InvalidTestCase[] { + const newCases: TSESLint.InvalidTestCase[] = []; + + for (const test of cases) { + for (const [formatLoose, names] of Object.entries(formatTestNames)) { + const format = [formatLoose as PredefinedFormatsString]; + for (const name of names.invalid) { + const createCase = ( + preparedName: string, + options: Selector, + messageId: MessageIds, + data: Record = {}, + ): TSESLint.InvalidTestCase => ({ + options: [ + { + ...options, + filter: '[iI]gnored', + }, + ], + code: `// ${JSON.stringify(options)}\n${test.code + .map(code => code.replace(REPLACE_REGEX, preparedName)) + .join('\n')}`, + errors: test.code.map(() => ({ + messageId, + ...(test.options.selector !== 'default' && + test.options.selector !== 'variableLike' && + test.options.selector !== 'memberLike' && + test.options.selector !== 'typeLike' + ? { + data: { + type: selectorTypeToMessageString(test.options.selector), + name: preparedName, + ...data, + }, + } + : // meta-types will use the correct selector, so don't assert on data shape + {}), + })), + }); + + const prefixSingle = ['MyPrefix']; + const prefixMulti = ['MyPrefix1', 'MyPrefix2']; + const suffixSingle = ['MySuffix']; + const suffixMulti = ['MySuffix1', 'MySuffix2']; + + newCases.push( + createCase( + name, + { + ...test.options, + format, + }, + 'doesNotMatchFormat', + { formats: format.join(', ') }, + ), + + // leadingUnderscore + createCase( + `_${name}`, + { + ...test.options, + format, + leadingUnderscore: 'forbid', + }, + 'unexpectedUnderscore', + { position: 'leading' }, + ), + createCase( + name, + { + ...test.options, + format, + leadingUnderscore: 'require', + }, + 'missingUnderscore', + { position: 'leading' }, + ), + + // trailingUnderscore + createCase( + `${name}_`, + { + ...test.options, + format, + trailingUnderscore: 'forbid', + }, + 'unexpectedUnderscore', + { position: 'trailing' }, + ), + createCase( + name, + { + ...test.options, + format, + trailingUnderscore: 'require', + }, + 'missingUnderscore', + { position: 'trailing' }, + ), + + // prefix + createCase( + name, + { + ...test.options, + format, + prefix: prefixSingle, + }, + 'missingAffix', + { position: 'prefix', affixes: prefixSingle.join(', ') }, + ), + createCase( + name, + { + ...test.options, + format, + prefix: prefixMulti, + }, + 'missingAffix', + { + position: 'prefix', + affixes: prefixMulti.join(', '), + }, + ), + + // suffix + createCase( + name, + { + ...test.options, + format, + suffix: suffixSingle, + }, + 'missingAffix', + { position: 'suffix', affixes: suffixSingle.join(', ') }, + ), + createCase( + name, + { + ...test.options, + format, + suffix: suffixMulti, + }, + 'missingAffix', + { + position: 'suffix', + affixes: suffixMulti.join(', '), + }, + ), + ); + } + } + } + + return newCases; +} + +const cases: Cases = [ + // #region default + { + code: [ + 'const % = 1;', + 'function % () {}', + '(function (%) {});', + 'class Ignored { constructor(private %) {} }', + 'const ignored = { % };', + 'interface Ignored { %: string }', + 'type Ignored = { %: string }', + 'class Ignored { private % = 1 }', + 'class Ignored { constructor(private %) {} }', + 'class Ignored { private %() {} }', + 'const ignored = { %() {} };', + 'class Ignored { private get %() {} }', + 'enum Ignored { % }', + 'abstract class % {}', + 'interface % { }', + 'type % = { };', + 'enum % {}', + 'interface Ignored<%> extends Ignored {}', + ], + options: { + selector: 'default', + filter: '[iI]gnored', + }, + }, + // #endregion default + + // #region variable + { + code: [ + 'const % = 1;', + 'let % = 1;', + 'var % = 1;', + 'const {%} = {ignored: 1};', + 'const {% = 2} = {ignored: 1};', + 'const {...%} = {ignored: 1};', + 'const [%] = [1];', + 'const [% = 1] = [1];', + 'const [...%] = [1];', + ], + options: { + selector: 'variable', + }, + }, + // #endregion variable + + // #region function + { + code: ['function % () {}', '(function % () {});', 'declare function % ();'], + options: { + selector: 'function', + }, + }, + // #endregion function + + // #region parameter + { + code: [ + 'function ignored(%) {}', + '(function (%) {});', + 'declare function ignored(%);', + 'function ignored({%}) {}', + 'function ignored(...%) {}', + 'function ignored({% = 1}) {}', + 'function ignored({...%}) {}', + 'function ignored([%]) {}', + 'function ignored([% = 1]) {}', + 'function ignored([...%]) {}', + ], + options: { + selector: 'parameter', + }, + }, + // #endregion parameter + + // #region property + { + code: [ + 'const ignored = { % };', + 'const ignored = { "%": 1 };', + 'interface Ignored { % }', + 'interface Ignored { "%": string }', + 'type Ignored = { % }', + 'type Ignored = { "%": string }', + 'class Ignored { private % }', + 'class Ignored { private "%" = 1 }', + 'class Ignored { private readonly % = 1 }', + 'class Ignored { private static % }', + 'class Ignored { private static readonly % = 1 }', + 'class Ignored { abstract % = 1 }', + 'class Ignored { declare % }', + ], + options: { + selector: 'property', + }, + }, + { + code: [ + 'class Ignored { abstract private static readonly % = 1; ignoredDueToModifiers = 1; }', + ], + options: { + selector: 'property', + modifiers: ['static', 'readonly'], + }, + }, + // #endregion property + + // #region parameterProperty + { + code: [ + 'class Ignored { constructor(private %) {} }', + 'class Ignored { constructor(readonly %) {} }', + 'class Ignored { constructor(private readonly %) {} }', + ], + options: { + selector: 'parameterProperty', + }, + }, + { + code: ['class Ignored { constructor(private readonly %) {} }'], + options: { + selector: 'parameterProperty', + modifiers: ['readonly'], + }, + }, + // #endregion parameterProperty + + // #region method + { + code: [ + 'const ignored = { %() {} };', + 'const ignored = { "%"() {} };', + 'const ignored = { %: () => {} };', + 'interface Ignored { %(): string }', + 'interface Ignored { "%"(): string }', + 'type Ignored = { %(): string }', + 'type Ignored = { "%"(): string }', + 'class Ignored { private %() {} }', + 'class Ignored { private "%"() {} }', + 'class Ignored { private readonly %() {} }', + 'class Ignored { private static %() {} }', + 'class Ignored { private static readonly %() {} }', + 'class Ignored { private % = () => {} }', + 'class Ignored { abstract %() }', + 'class Ignored { declare %() }', + ], + options: { + selector: 'method', + }, + }, + { + code: [ + 'class Ignored { abstract private static %() {}; ignoredDueToModifiers() {}; }', + ], + options: { + selector: 'method', + modifiers: ['abstract', 'static'], + }, + }, + // #endregion method + + // #region accessor + { + code: [ + 'const ignored = { get %() {} };', + 'const ignored = { set "%"(ignored) {} };', + 'class Ignored { private get %() {} }', + 'class Ignored { private set "%"(ignored) {} }', + 'class Ignored { private static get %() {} }', + ], + options: { + selector: 'accessor', + }, + }, + { + code: [ + 'class Ignored { private static get %() {}; get ignoredDueToModifiers() {}; }', + ], + options: { + selector: 'accessor', + modifiers: ['private', 'static'], + }, + }, + // #endregion accessor + + // #region enumMember + { + code: ['enum Ignored { % }', 'enum Ignored { "%" }'], + options: { + selector: 'enumMember', + }, + }, + // #endregion enumMember + + // #region class + { + code: ['class % {}', 'abstract class % {}', 'const ignored = class % {}'], + options: { + selector: 'class', + }, + }, + { + code: ['abstract class % {}; class ignoredDueToModifier {}'], + options: { + selector: 'class', + modifiers: ['abstract'], + }, + }, + // #endregion class + + // #region interface + { + code: ['interface % {}'], + options: { + selector: 'interface', + }, + }, + // #endregion interface + + // #region typeAlias + { + code: ['type % = {};', 'type % = 1;'], + options: { + selector: 'typeAlias', + }, + }, + // #endregion typeAlias + + // #region enum + { + code: ['enum % {}'], + options: { + selector: 'enum', + }, + }, + // #endregion enum + + // #region typeParameter + { + code: [ + 'class Ignored<%> {}', + 'function ignored<%>() {}', + 'type Ignored<%> = { ignored: % };', + 'interface Ignored<%> extends Ignored {}', + ], + options: { + selector: 'typeParameter', + }, + }, + // #endregion typeParameter +]; + +ruleTester.run('naming-convention', rule, { + valid: [ + ...createValidTestCases(cases), + { + code: ` + declare const string_camelCase: string; + declare const string_camelCase: string | null; + declare const string_camelCase: string | null | undefined; + declare const string_camelCase: 'a' | null | undefined; + declare const string_camelCase: string | 'a' | null | undefined; + + declare const number_camelCase: number; + declare const number_camelCase: number | null; + declare const number_camelCase: number | null | undefined; + declare const number_camelCase: 1 | null | undefined; + declare const number_camelCase: number | 2 | null | undefined; + + declare const boolean_camelCase: boolean; + declare const boolean_camelCase: boolean | null; + declare const boolean_camelCase: boolean | null | undefined; + declare const boolean_camelCase: true | null | undefined; + declare const boolean_camelCase: false | null | undefined; + declare const boolean_camelCase: true | false | null | undefined; + `, + parserOptions, + options: [ + { + selector: 'variable', + types: ['string'], + format: ['camelCase'], + prefix: ['string_'], + }, + { + selector: 'variable', + types: ['number'], + format: ['camelCase'], + prefix: ['number_'], + }, + { + selector: 'variable', + types: ['boolean'], + format: ['camelCase'], + prefix: ['boolean_'], + }, + ], + }, + { + code: ` + let foo = 'a'; + const _foo = 1; + interface Foo {} + class Bar {} + function foo_function_bar() {} + `, + options: [ + { + selector: 'default', + format: ['camelCase'], + custom: { + regex: /^unused_\w/.source, + match: false, + }, + leadingUnderscore: 'allow', + }, + { + selector: 'typeLike', + format: ['PascalCase'], + custom: { + regex: /^I[A-Z]/.source, + match: false, + }, + }, + { + selector: 'function', + format: ['snake_case'], + custom: { + regex: /_function_/.source, + match: true, + }, + leadingUnderscore: 'allow', + }, + ], + }, + ], + invalid: [ + ...createInvalidTestCases(cases), + { + code: ` + declare const string_camelCase01: string; + declare const string_camelCase02: string | null; + declare const string_camelCase03: string | null | undefined; + declare const string_camelCase04: 'a' | null | undefined; + declare const string_camelCase05: string | 'a' | null | undefined; + + declare const number_camelCase06: number; + declare const number_camelCase07: number | null; + declare const number_camelCase08: number | null | undefined; + declare const number_camelCase09: 1 | null | undefined; + declare const number_camelCase10: number | 2 | null | undefined; + + declare const boolean_camelCase11: boolean; + declare const boolean_camelCase12: boolean | null; + declare const boolean_camelCase13: boolean | null | undefined; + declare const boolean_camelCase14: true | null | undefined; + declare const boolean_camelCase15: false | null | undefined; + declare const boolean_camelCase16: true | false | null | undefined; + `, + options: [ + { + selector: 'variable', + types: ['string'], + format: ['snake_case'], + prefix: ['string_'], + }, + { + selector: 'variable', + types: ['number'], + format: ['snake_case'], + prefix: ['number_'], + }, + { + selector: 'variable', + types: ['boolean'], + format: ['snake_case'], + prefix: ['boolean_'], + }, + ], + parserOptions, + errors: Array(16).fill({ messageId: 'doesNotMatchFormat' }), + }, + { + code: ` + declare const function_camelCase1: (() => void); + declare const function_camelCase2: (() => void) | null; + declare const function_camelCase3: (() => void) | null | undefined; + declare const function_camelCase4: (() => void) | (() => string) | null | undefined; + `, + options: [ + { + selector: 'variable', + types: ['function'], + format: ['snake_case'], + prefix: ['function_'], + }, + ], + parserOptions, + errors: Array(4).fill({ messageId: 'doesNotMatchFormat' }), + }, + { + code: ` + declare const array_camelCase1: Array; + declare const array_camelCase2: ReadonlyArray | null; + declare const array_camelCase3: number[] | null | undefined; + declare const array_camelCase4: readonly number[] | null | undefined; + declare const array_camelCase5: number[] | (number | string)[] | null | undefined; + declare const array_camelCase6: [] | null | undefined; + declare const array_camelCase7: [number] | null | undefined; + + declare const array_camelCase8: readonly number[] | Array | [boolean] | null | undefined; + `, + options: [ + { + selector: 'variable', + types: ['array'], + format: ['snake_case'], + prefix: ['array_'], + }, + ], + parserOptions, + errors: Array(8).fill({ messageId: 'doesNotMatchFormat' }), + }, + { + code: ` + let unused_foo = 'a'; + const _unused_foo = 1; + interface IFoo {} + class IBar {} + function fooBar() {} + `, + options: [ + { + selector: 'default', + format: ['snake_case'], + custom: { + regex: /^unused_\w/.source, + match: false, + }, + leadingUnderscore: 'allow', + }, + { + selector: 'typeLike', + format: ['PascalCase'], + custom: { + regex: /^I[A-Z]/.source, + match: false, + }, + }, + { + selector: 'function', + format: ['camelCase'], + custom: { + regex: /function/.source, + match: true, + }, + leadingUnderscore: 'allow', + }, + ], + errors: [ + { + messageId: 'satisfyCustom', + line: 2, + data: { + type: 'Variable', + name: 'unused_foo', + regex: '/^unused_\\w/', + regexMatch: 'not match', + }, + }, + { + messageId: 'satisfyCustom', + line: 3, + data: { + type: 'Variable', + name: '_unused_foo', + regex: '/^unused_\\w/', + regexMatch: 'not match', + }, + }, + { + messageId: 'satisfyCustom', + line: 4, + data: { + type: 'Interface', + name: 'IFoo', + regex: '/^I[A-Z]/', + regexMatch: 'not match', + }, + }, + { + messageId: 'satisfyCustom', + line: 5, + data: { + type: 'Class', + name: 'IBar', + regex: '/^I[A-Z]/', + regexMatch: 'not match', + }, + }, + { + messageId: 'satisfyCustom', + line: 6, + data: { + type: 'Function', + name: 'fooBar', + regex: '/function/', + regexMatch: 'match', + }, + }, + ], + }, + ], +}); diff --git a/packages/eslint-plugin/tests/rules/no-magic-numbers.test.ts b/packages/eslint-plugin/tests/rules/no-magic-numbers.test.ts index f65610ea3f4b..9c8bdeae10a8 100644 --- a/packages/eslint-plugin/tests/rules/no-magic-numbers.test.ts +++ b/packages/eslint-plugin/tests/rules/no-magic-numbers.test.ts @@ -34,11 +34,14 @@ ruleTester.run('no-magic-numbers', rule, { options: [{ ignoreNumericLiteralTypes: true }], }, { - code: 'enum foo { SECOND = 1000 }', - options: [{ ignoreEnums: true }], - }, - { - code: 'enum foo { SECOND = 1000, NUM = "0123456789" }', + code: ` + enum foo { + SECOND = 1000, + NUM = "0123456789", + NEG = -1, + POS = +1, + } + `, options: [{ ignoreEnums: true }], }, { @@ -152,7 +155,14 @@ class Foo { ], }, { - code: 'enum foo { SECOND = 1000 }', + code: ` +enum foo { + SECOND = 1000, + NUM = "0123456789", + NEG = -1, + POS = +1, +} + `, options: [{ ignoreEnums: false }], errors: [ { @@ -160,22 +170,24 @@ class Foo { data: { raw: '1000', }, - line: 1, - column: 21, + line: 3, + column: 12, }, - ], - }, - { - code: 'enum foo { SECOND = 1000, NUM = "0123456789" }', - options: [{ ignoreEnums: false }], - errors: [ { messageId: 'noMagic', data: { - raw: '1000', + raw: '-1', }, - line: 1, - column: 21, + line: 5, + column: 9, + }, + { + messageId: 'noMagic', + data: { + raw: '1', + }, + line: 6, + column: 10, }, ], }, @@ -184,43 +196,61 @@ class Foo { class Foo { readonly A = 1; readonly B = 2; - public static readonly C = 1; - static readonly D = 1; - readonly E = -1; - readonly F = +1; + public static readonly C = 3; + static readonly D = 4; + readonly E = -5; + readonly F = +6; } `, options: [{ ignoreReadonlyClassProperties: false }], errors: [ { messageId: 'noMagic', + data: { + raw: '1', + }, line: 3, column: 16, }, { messageId: 'noMagic', + data: { + raw: '2', + }, line: 4, column: 16, }, { messageId: 'noMagic', + data: { + raw: '3', + }, line: 5, column: 30, }, { messageId: 'noMagic', + data: { + raw: '4', + }, line: 6, column: 23, }, { messageId: 'noMagic', + data: { + raw: '-5', + }, line: 7, column: 16, }, { messageId: 'noMagic', + data: { + raw: '6', + }, line: 8, - column: 16, + column: 17, }, ], }, diff --git a/packages/eslint-plugin/tests/rules/no-throw-literal.test.ts b/packages/eslint-plugin/tests/rules/no-throw-literal.test.ts index 359c2abf3c51..451e88262a31 100644 --- a/packages/eslint-plugin/tests/rules/no-throw-literal.test.ts +++ b/packages/eslint-plugin/tests/rules/no-throw-literal.test.ts @@ -14,8 +14,8 @@ const ruleTester = new RuleTester({ ruleTester.run('no-throw-literal', rule, { valid: [ 'throw new Error();', - "throw new Error('error');", - "throw Error('error');", + 'throw new Error("error");', + 'throw Error("error");', ` const e = new Error(); throw e; @@ -65,16 +65,28 @@ throw new CustomError(); `, 'throw foo = new Error();', 'throw 1, 2, new Error();', - "throw 'literal' && new Error();", - "throw new Error() || 'literal'", - "throw foo ? new Error() : 'literal';", - "throw foo ? 'literal' : new Error();", + 'throw "literal" && new Error();', + 'throw new Error() || "literal"', + 'throw foo ? new Error() : "literal";', + 'throw foo ? "literal" : new Error();', 'function* foo() { let index = 0; throw yield index++; }', 'async function foo() { throw await bar; }', ` import { Error } from './missing'; throw Error; `, + ` +class CustomError extends Error {} +throw new CustomError(); + `, + ` +class CustomError extends Error {} +throw new CustomError(); + `, + ` +class CustomError extends Error {} +throw new CustomError(); + `, ], invalid: [ { @@ -86,7 +98,7 @@ throw Error; ], }, { - code: "throw new String('');", + code: 'throw new String("");', errors: [ { messageId: 'object', @@ -94,7 +106,7 @@ throw Error; ], }, { - code: "throw 'error';", + code: 'throw "error";', errors: [ { messageId: 'object', @@ -134,7 +146,7 @@ throw Error; ], }, { - code: "throw 'a' + 'b';", + code: 'throw "a" + "b";', errors: [ { messageId: 'object', @@ -153,7 +165,7 @@ throw a + 'b'; ], }, { - code: "throw foo = 'error';", + code: 'throw foo = "error";', errors: [ { messageId: 'object', @@ -169,7 +181,7 @@ throw a + 'b'; ], }, { - code: "throw 'literal' && 'not an Error';", + code: 'throw "literal" && "not an Error";', errors: [ { messageId: 'object', @@ -177,7 +189,7 @@ throw a + 'b'; ], }, { - code: "throw foo ? 'not an Error' : 'literal';", + code: 'throw foo ? "not an Error" : "literal";', errors: [ { messageId: 'object', @@ -288,5 +300,18 @@ throw new Error(); }, ], }, + { + code: ` +class CustomError extends Foo {} +throw new CustomError(); + `, + errors: [ + { + messageId: 'object', + line: 3, + column: 7, + }, + ], + }, ], }); diff --git a/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts b/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts index e2e466962249..1a7abc30f465 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts @@ -88,6 +88,52 @@ function test(t: T | []) { function test(a: string) { return a === "a" }`, + + /** + * Predicate functions + **/ + // valid, with the flag off + ` +[1,3,5].filter(() => true); +[1,2,3].find(() => false); +function truthy() { + return []; +} +function falsy() {} +[1,3,5].filter(truthy); +[1,2,3].find(falsy); +`, + { + options: [{ checkArrayPredicates: true }], + code: ` +// with literal arrow function +[0,1,2].filter(x => x); + +// filter with named function +function length(x: string) { + return x.length; +} +["a", "b", ""].filter(length); + +// with non-literal array +function nonEmptyStrings(x: string[]) { + return x.filter(length); +} +`, + }, + // Ignores non-array methods of the same name + { + options: [{ checkArrayPredicates: true }], + code: ` +const notArray = { + filter: (func: () => boolean) => func(), + find: (func: () => boolean) => func(), +}; +notArray.filter(() => true); +notArray.find(() => true); +`, + }, + // Nullish coalescing operator ` function test(a: string | null) { @@ -289,6 +335,63 @@ function test(a: never) { errors: [ruleError(3, 10, 'never')], }, + // Predicate functions + { + options: [{ checkArrayPredicates: true }], + code: ` +[1,3,5].filter(() => true); +[1,2,3].find(() => { return false; }); + +// with non-literal array +function nothing(x: string[]) { + return x.filter(() => false); +} +// with readonly array +function nothing2(x: readonly string[]) { + return x.filter(() => false); +} +// with tuple +function nothing3(x: [string, string]) { + return x.filter(() => false); +} +`, + errors: [ + ruleError(2, 22, 'alwaysTruthy'), + ruleError(3, 29, 'alwaysFalsy'), + ruleError(7, 25, 'alwaysFalsy'), + ruleError(11, 25, 'alwaysFalsy'), + ruleError(15, 25, 'alwaysFalsy'), + ], + }, + { + options: [{ checkArrayPredicates: true }], + code: ` +function truthy() { + return []; +} +function falsy() {} +[1,3,5].filter(truthy); +[1,2,3].find(falsy); +`, + errors: [ + ruleError(6, 16, 'alwaysTruthyFunc'), + ruleError(7, 14, 'alwaysFalsyFunc'), + ], + }, + // Supports generics + // TODO: fix this + // { + // options: [{ checkArrayPredicates: true }], + // code: ` + // const isTruthy = (t: T) => T; + // // Valid: numbers can be truthy or falsy (0). + // [0,1,2,3].filter(isTruthy); + // // Invalid: arrays are always falsy. + // [[1,2], [3,4]].filter(isTruthy); + // `, + // errors: [ruleError(6, 23, 'alwaysTruthyFunc')], + // }, + // Still errors on in the expected locations when ignoring RHS { options: [{ ignoreRhs: true }], diff --git a/packages/eslint-plugin/tests/rules/no-unnecessary-type-assertion.test.ts b/packages/eslint-plugin/tests/rules/no-unnecessary-type-assertion.test.ts index b4f079f39605..3d5c958c3cc6 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-type-assertion.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-type-assertion.test.ts @@ -117,6 +117,19 @@ function testFunction(_param: string | null): void { /* noop */ } const value = 'test' as string | null | undefined testFunction(value!) `, + // https://github.com/typescript-eslint/typescript-eslint/issues/982 + { + code: ` +declare namespace JSX { interface IntrinsicElements { div: { key?: string | number } } } + +function Test(props: { + id?: null | string | number; +}) { + return
; +} + `, + filename: 'react.tsx', + }, ], invalid: [ @@ -327,5 +340,33 @@ class Mx { }, ], }, + // https://github.com/typescript-eslint/typescript-eslint/issues/982 + { + code: ` +declare namespace JSX { interface IntrinsicElements { div: { key?: string | number } } } + +function Test(props: { + id?: string | number; +}) { + return
; +} + `, + output: ` +declare namespace JSX { interface IntrinsicElements { div: { key?: string | number } } } + +function Test(props: { + id?: string | number; +}) { + return
; +} + `, + errors: [ + { + messageId: 'contextuallyUnnecessary', + line: 7, + }, + ], + filename: 'react.tsx', + }, ], }); diff --git a/packages/eslint-plugin/tests/util.test.ts b/packages/eslint-plugin/tests/util.test.ts index 449cfc9b1adb..b98111d9a28a 100644 --- a/packages/eslint-plugin/tests/util.test.ts +++ b/packages/eslint-plugin/tests/util.test.ts @@ -1,5 +1,3 @@ -import assert from 'assert'; - import * as util from '../src/util'; describe('isDefinitionFile', () => { @@ -20,7 +18,7 @@ describe('isDefinitionFile', () => { ]; invalid.forEach(f => { - assert.strictEqual(util.isDefinitionFile(f), false); + expect(util.isDefinitionFile(f)).toStrictEqual(false); }); }); @@ -28,13 +26,13 @@ describe('isDefinitionFile', () => { const valid = ['test.d.ts', 'test.d.tsx', 'test.D.TS', 'test.D.TSX']; valid.forEach(f => { - assert.strictEqual(util.isDefinitionFile(f), true); + expect(util.isDefinitionFile(f)).toStrictEqual(true); }); }); }); describe('upperCaseFirst', () => { it('upper cases first', () => { - assert.strictEqual(util.upperCaseFirst('hello'), 'Hello'); + expect(util.upperCaseFirst('hello')).toStrictEqual('Hello'); }); }); diff --git a/packages/eslint-plugin/tools/validate-configs/checkConfigAll.ts b/packages/eslint-plugin/tools/validate-configs/checkConfigAll.ts deleted file mode 100644 index fab57fe20cf6..000000000000 --- a/packages/eslint-plugin/tools/validate-configs/checkConfigAll.ts +++ /dev/null @@ -1,35 +0,0 @@ -import plugin from '../../src/index'; -import { logRule } from '../log'; - -const prefix = '@typescript-eslint/'; - -function checkConfigAll(): boolean { - const { rules } = plugin; - - const all = plugin.configs.all.rules; - const allNames = new Set(Object.keys(all)); - - return Object.entries(rules).reduce((acc, [ruleName, rule]) => { - if (!rule.meta.deprecated) { - const prefixed = `${prefix}${ruleName}` as keyof typeof all; - if (allNames.has(prefixed)) { - if (all[prefixed] !== 'error') { - logRule( - false, - ruleName, - 'incorrect setting compared to the rule meta.', - ); - return true; - } - } else { - logRule(false, ruleName, 'missing in the config.'); - return true; - } - } - - logRule(true, ruleName); - return acc; - }, false); -} - -export { checkConfigAll }; diff --git a/packages/eslint-plugin/tools/validate-configs/checkConfigRecommended.ts b/packages/eslint-plugin/tools/validate-configs/checkConfigRecommended.ts deleted file mode 100644 index 28f0c7b21a50..000000000000 --- a/packages/eslint-plugin/tools/validate-configs/checkConfigRecommended.ts +++ /dev/null @@ -1,39 +0,0 @@ -import plugin from '../../src/index'; -import { logRule } from '../log'; - -const prefix = '@typescript-eslint/'; - -function checkConfigRecommended(): boolean { - const { rules } = plugin; - - const recommended = plugin.configs.recommended.rules; - const recommendedNames = new Set(Object.keys(recommended)); - - return Object.entries(rules).reduce((acc, [ruleName, rule]) => { - if ( - !rule.meta.deprecated && - rule.meta.docs.recommended !== false && - rule.meta.docs.requiresTypeChecking !== true - ) { - const prefixed = `${prefix}${ruleName}` as keyof typeof recommended; - if (recommendedNames.has(prefixed)) { - if (recommended[prefixed] !== rule.meta.docs.recommended) { - logRule( - false, - ruleName, - 'incorrect setting compared to the rule meta.', - ); - return true; - } - } else { - logRule(false, ruleName, 'missing in the config.'); - return true; - } - } - - logRule(true, ruleName); - return acc; - }, false); -} - -export { checkConfigRecommended }; diff --git a/packages/eslint-plugin/tools/validate-configs/checkConfigRecommendedRequiringTypeChecking.ts b/packages/eslint-plugin/tools/validate-configs/checkConfigRecommendedRequiringTypeChecking.ts deleted file mode 100644 index a63f5e42c236..000000000000 --- a/packages/eslint-plugin/tools/validate-configs/checkConfigRecommendedRequiringTypeChecking.ts +++ /dev/null @@ -1,45 +0,0 @@ -import plugin from '../../src/index'; -import { logRule } from '../log'; - -const prefix = '@typescript-eslint/'; - -function checkConfigRecommendedRequiringTypeChecking(): boolean { - const { rules } = plugin; - - const recommendedRequiringTypeChecking = - plugin.configs['recommended-requiring-type-checking'].rules; - const recommendedNames = new Set( - Object.keys(recommendedRequiringTypeChecking), - ); - - return Object.entries(rules).reduce((acc, [ruleName, rule]) => { - if ( - !rule.meta.deprecated && - rule.meta.docs.recommended !== false && - rule.meta.docs.requiresTypeChecking === true - ) { - const prefixed = `${prefix}${ruleName}` as keyof typeof recommendedRequiringTypeChecking; - if (recommendedNames.has(prefixed)) { - if ( - recommendedRequiringTypeChecking[prefixed] !== - rule.meta.docs.recommended - ) { - logRule( - false, - ruleName, - 'incorrect setting compared to the rule meta.', - ); - return true; - } - } else { - logRule(false, ruleName, 'missing in the config.'); - return true; - } - } - - logRule(true, ruleName); - return acc; - }, false); -} - -export { checkConfigRecommendedRequiringTypeChecking }; diff --git a/packages/eslint-plugin/tools/validate-configs/index.ts b/packages/eslint-plugin/tools/validate-configs/index.ts deleted file mode 100644 index 1c05196a677e..000000000000 --- a/packages/eslint-plugin/tools/validate-configs/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -import chalk from 'chalk'; -import { checkConfigRecommended } from './checkConfigRecommended'; -import { checkConfigRecommendedRequiringTypeChecking } from './checkConfigRecommendedRequiringTypeChecking'; -import { checkConfigAll } from './checkConfigAll'; - -let hasErrors = false; -console.log(chalk.underline('Checking config "recommended"')); -hasErrors = checkConfigRecommended() || hasErrors; - -console.log( - chalk.underline('Checking config "recommended-requiring-type-checking"'), -); -hasErrors = checkConfigRecommendedRequiringTypeChecking() || hasErrors; - -console.log(); -console.log(chalk.underline('Checking config "all"')); -hasErrors = checkConfigAll() || hasErrors; - -if (hasErrors) { - console.log('\n\n'); - console.error( - chalk.bold.bgRed.white('There were errors found in the configs'), - ); - console.error(`Please run ${chalk.inverse('yarn generate:configs')}`); - console.log('\n\n'); - // eslint-disable-next-line no-process-exit - process.exit(1); -} diff --git a/packages/eslint-plugin/tools/validate-docs/check-for-rule-docs.ts b/packages/eslint-plugin/tools/validate-docs/check-for-rule-docs.ts deleted file mode 100644 index f52c1bccbb0d..000000000000 --- a/packages/eslint-plugin/tools/validate-docs/check-for-rule-docs.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { TSESLint } from '@typescript-eslint/experimental-utils'; -import fs from 'fs'; -import path from 'path'; -import { logRule } from '../log'; -import chalk from 'chalk'; -import marked from 'marked'; - -function isHeading( - token: marked.Token, - depth = 1, -): token is marked.Tokens.Heading { - return token && token.type === 'heading' && token.depth === depth; -} - -export function checkForRuleDocs( - rules: Record>>, -): boolean { - const docsRoot = path.resolve(__dirname, '../../docs/rules'); - const ruleDocs = new Set(fs.readdirSync(docsRoot)); - - let hasErrors = false; - Object.keys(rules).forEach(ruleName => { - const rule = rules[ruleName].meta; - const errors: string[] = []; - const ruleHasDoc = ruleDocs.has(`${ruleName}.md`); - if (!ruleHasDoc) { - errors.push(`Couldn't find file docs/rules/${ruleName}.md`); - } else { - const file = fs.readFileSync( - path.join(docsRoot, `${ruleName}.md`), - 'utf-8', - ); - - const tokens = marked.lexer(file, { - gfm: true, - silent: false, - }); - - if (isHeading(tokens[0])) { - const expectedDescription = `${rule.docs.description} (\`${ruleName}\`)`; - if (tokens[0].text !== expectedDescription) { - errors.push( - 'Rule title does not match the rule metadata.', - ` Expected: ${chalk.underline(expectedDescription)}`, - ` Received: ${chalk.underline(tokens[0].text)}`, - ); - } - } else { - errors.push('Rule title not found.'); - } - } - - const ruleConfigName = /([A-Za-z-]+)\.md/.exec(rule.docs.url) ?? []; - if (ruleConfigName[1] !== ruleName) { - errors.push( - 'Name field does not match with rule name.', - ` Expected: ${chalk.underline(ruleName)}`, - ` Received: ${chalk.underline(ruleConfigName[1])}`, - ); - } - - logRule(errors.length === 0, ruleName, ...errors); - hasErrors = hasErrors || errors.length !== 0; - }); - - return hasErrors; -} diff --git a/packages/eslint-plugin/tools/validate-docs/index.ts b/packages/eslint-plugin/tools/validate-docs/index.ts deleted file mode 100644 index 5f2cf1fa9f8b..000000000000 --- a/packages/eslint-plugin/tools/validate-docs/index.ts +++ /dev/null @@ -1,34 +0,0 @@ -import chalk from 'chalk'; -import plugin from '../../src/index'; -import { checkForRuleDocs } from './check-for-rule-docs'; -import { parseReadme } from './parse-readme'; -import { validateTableStructure } from './validate-table-structure'; -import { validateTableRules } from './validate-table-rules'; - -const { rules } = plugin; - -let hasErrors = false; -console.log(chalk.underline('Checking for rule docs')); -hasErrors = hasErrors || checkForRuleDocs(rules); - -console.log(); -console.log(chalk.underline('Validating README.md')); -const rulesTable = parseReadme(); - -console.log(); -console.log(chalk.italic('Checking table structure...')); -hasErrors = hasErrors || validateTableStructure(rules, rulesTable); - -console.log(); -console.log(chalk.italic('Checking rules...')); -hasErrors = hasErrors || validateTableRules(rules, rulesTable); - -if (hasErrors) { - console.log('\n\n'); - console.error( - chalk.bold.bgRed.white('There were errors found in the documentation.'), - ); - console.log('\n\n'); - // eslint-disable-next-line no-process-exit - process.exit(1); -} diff --git a/packages/eslint-plugin/tools/validate-docs/parse-readme.ts b/packages/eslint-plugin/tools/validate-docs/parse-readme.ts deleted file mode 100644 index aace3736fcbe..000000000000 --- a/packages/eslint-plugin/tools/validate-docs/parse-readme.ts +++ /dev/null @@ -1,28 +0,0 @@ -import fs from 'fs'; -import marked from 'marked'; -import path from 'path'; - -function parseReadme(): marked.Tokens.Table { - const readmeRaw = fs.readFileSync( - path.resolve(__dirname, '../../README.md'), - 'utf8', - ); - const readme = marked.lexer(readmeRaw, { - gfm: true, - silent: false, - }); - - // find the table - const rulesTable = readme.find( - token => token.type === 'table', - ) as marked.Tokens.Table; - if (!rulesTable) { - console.error('Could not find the rules table in README.md'); - // eslint-disable-next-line no-process-exit - process.exit(1); - } - - return rulesTable; -} - -export { parseReadme }; diff --git a/packages/eslint-plugin/tools/validate-docs/validate-table-rules.ts b/packages/eslint-plugin/tools/validate-docs/validate-table-rules.ts deleted file mode 100644 index b85172d0fdc4..000000000000 --- a/packages/eslint-plugin/tools/validate-docs/validate-table-rules.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { TSESLint } from '@typescript-eslint/experimental-utils'; -import chalk from 'chalk'; -import fs from 'fs'; -import marked from 'marked'; -import path from 'path'; -import { logRule } from '../log'; - -function validateTableRules( - rules: Record>>, - rulesTable: marked.Tokens.Table, -): boolean { - let hasErrors = false; - - Object.entries(rules).forEach(([ruleName, rule]) => { - const row = rulesTable.cells.find(row => - row[0].includes(`/${ruleName}.md`), - ); - - if (!row) { - if (!rule.meta.deprecated) { - hasErrors = true; - logRule(false, ruleName, 'Missing entry in table'); - return; - } - - // all is well, the rule shouldn't have a row as it's deprecated - return; - } - if (row && rule.meta.deprecated) { - hasErrors = true; - logRule( - false, - ruleName, - 'Rule is marked as deprecated, should not have an entry in the table', - ); - return; - } - - const errors: string[] = []; - const [ - rowLink, - rowDescription, - rowIsRecommended, - rowIsFixable, - rowNeedsTypeInfo, - ] = row; - - function validateTableBoolean( - value: boolean, - cell: string, - trueString: string, - columnLabel: string, - ): void { - if (value && cell !== trueString) { - errors.push( - `Rule ${chalk.red( - 'not', - )} marked as ${columnLabel} when it ${chalk.bold('should')} be`, - ); - } - - if (!value && cell !== '') { - errors.push( - `Rule ${chalk.red( - 'was', - )} marked as ${columnLabel} when it ${chalk.bold('should not')} be`, - ); - } - } - - const expectedLink = `[\`@typescript-eslint/${ruleName}\`](./docs/rules/${ruleName}.md)`; - if (rowLink !== expectedLink) { - errors.push( - `Link is invalid.`, - ` Expected: ${chalk.underline(expectedLink)}`, - ` Received: ${chalk.underline(rowLink)}`, - ); - } - - const expectedDescription = rule.meta.docs.description; - if (rowDescription !== expectedDescription) { - errors.push( - 'Description does not match the rule metadata.', - ` Expected: ${chalk.underline(expectedDescription)}`, - ` Received: ${chalk.underline(rowDescription)}`, - ); - } - - validateTableBoolean( - !!rule.meta.docs.recommended, - rowIsRecommended, - ':heavy_check_mark:', - 'recommended', - ); - - validateTableBoolean( - rule.meta.fixable !== undefined, - rowIsFixable, - ':wrench:', - 'fixable', - ); - - // quick-and-dirty check to see if it uses parserServices - // not perfect but should be good enough - const ruleFileContents = fs.readFileSync( - path.resolve(__dirname, `../../src/rules/${ruleName}.ts`), - ); - - const usesTypeInformation = ruleFileContents.includes('getParserServices'); - const tableRowHasThoughtBalloon = !!rowNeedsTypeInfo; - if (rule.meta.docs.requiresTypeChecking === true) { - if (!usesTypeInformation) { - errors.push( - 'Rule has `requiresTypeChecking` set in its meta, but it does not actually use type information - fix by removing `meta.docs.requiresTypeChecking`', - ); - } else if (!tableRowHasThoughtBalloon) { - errors.push( - 'Rule was documented as not using type information, when it actually does - fix by updating the plugin README.md', - ); - } - } else { - if (usesTypeInformation) { - errors.push( - 'Rule does not have `requiresTypeChecking` set in its meta, despite using type information - fix by setting `meta.docs.requiresTypeChecking: true` in the rule', - ); - } else if (tableRowHasThoughtBalloon) { - errors.push( - `Rule was documented as using type information, when it actually doesn't - fix by updating the plugin README.md`, - ); - } - } - - validateTableBoolean( - usesTypeInformation, - rowNeedsTypeInfo, - ':thought_balloon:', - 'requiring type information', - ); - - hasErrors = hasErrors || errors.length > 0; - logRule(errors.length === 0, ruleName, ...errors); - }); - - return hasErrors; -} - -export { validateTableRules }; diff --git a/packages/eslint-plugin/tools/validate-docs/validate-table-structure.ts b/packages/eslint-plugin/tools/validate-docs/validate-table-structure.ts deleted file mode 100644 index cd5828a8e36f..000000000000 --- a/packages/eslint-plugin/tools/validate-docs/validate-table-structure.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { TSESLint } from '@typescript-eslint/experimental-utils'; -import chalk from 'chalk'; -import marked from 'marked'; -import { logError } from '../log'; - -const RULE_LINK_REGEX = /\[`@typescript-eslint\/(.+)`\]/; -function validateTableStructure( - rules: Record>>, - rulesTable: marked.Tokens.Table, -): boolean { - const ruleNames = Object.keys(rules) - .filter(ruleName => rules[ruleName].meta.deprecated !== true) - .sort(); - let hasErrors = false; - - rulesTable.cells.forEach((row, rowIndex) => { - const match = RULE_LINK_REGEX.exec(row[0]); - if (!match) { - logError(chalk.bold(`Unable to parse link in row ${rowIndex}:`), row[0]); - hasErrors = true; - return; - } - - const rowRuleName = match[1]; - const ruleIndex = ruleNames.findIndex(ruleName => rowRuleName === ruleName); - if (ruleIndex === -1) { - logError( - chalk.bold( - `Found rule ${rowRuleName} in table, but it doesn't exist in the plugin.`, - ), - ); - hasErrors = true; - return; - } - - if (ruleIndex !== rowIndex) { - console.error( - chalk.bold.red('✗'), - chalk.bold('Sorting:'), - 'Incorrect row index for', - chalk.bold(rowRuleName), - 'expected', - ruleIndex, - 'got', - rowIndex, - ); - hasErrors = true; - return; - } - }); - - return hasErrors; -} - -export { validateTableStructure }; diff --git a/packages/eslint-plugin/typings/typescript.d.ts b/packages/eslint-plugin/typings/typescript.d.ts new file mode 100644 index 000000000000..6d9a098b538b --- /dev/null +++ b/packages/eslint-plugin/typings/typescript.d.ts @@ -0,0 +1,21 @@ +import { TypeChecker, Type } from '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): boolean; + /** + * @returns `true` if the given type is a tuple type: + * - [foo] + */ + isTupleType(type: Type): boolean; + } +} diff --git a/packages/experimental-utils/CHANGELOG.md b/packages/experimental-utils/CHANGELOG.md index 88cec1802a9d..bb3baf882752 100644 --- a/packages/experimental-utils/CHANGELOG.md +++ b/packages/experimental-utils/CHANGELOG.md @@ -3,6 +3,18 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.16.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.15.0...v2.16.0) (2020-01-13) + + +### Features + +* **typescript-estree:** add parserOption to turn on debug logs ([#1413](https://github.com/typescript-eslint/typescript-eslint/issues/1413)) ([25092fd](https://github.com/typescript-eslint/typescript-eslint/commit/25092fd)) +* **typescript-estree:** add strict type mapping to esTreeNodeToTSNodeMap ([#1382](https://github.com/typescript-eslint/typescript-eslint/issues/1382)) ([d3d70a3](https://github.com/typescript-eslint/typescript-eslint/commit/d3d70a3)) + + + + + # [2.15.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.14.0...v2.15.0) (2020-01-06) **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 34b7f5e91b7a..eaf57fde6a17 100644 --- a/packages/experimental-utils/package.json +++ b/packages/experimental-utils/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/experimental-utils", - "version": "2.15.0", + "version": "2.16.0", "description": "(Experimental) Utilities for working with TypeScript + ESLint together", "keywords": [ "eslint", @@ -26,6 +26,7 @@ }, "license": "MIT", "main": "dist/index.js", + "types": "dist/index.d.ts", "scripts": { "build": "tsc -b tsconfig.build.json", "clean": "tsc -b tsconfig.build.json --clean", @@ -36,7 +37,7 @@ }, "dependencies": { "@types/json-schema": "^7.0.3", - "@typescript-eslint/typescript-estree": "2.15.0", + "@typescript-eslint/typescript-estree": "2.16.0", "eslint-scope": "^5.0.0" }, "peerDependencies": { diff --git a/packages/experimental-utils/src/ts-eslint/ParserOptions.ts b/packages/experimental-utils/src/ts-eslint/ParserOptions.ts index 87f919129053..12c121989dc5 100644 --- a/packages/experimental-utils/src/ts-eslint/ParserOptions.ts +++ b/packages/experimental-utils/src/ts-eslint/ParserOptions.ts @@ -1,3 +1,5 @@ +import { TSESTreeOptions } from '@typescript-eslint/typescript-estree'; + export interface ParserOptions { comment?: boolean; ecmaFeatures?: { @@ -9,6 +11,7 @@ export interface ParserOptions { errorOnUnknownASTType?: boolean; extraFileExtensions?: string[]; // ts-estree specific + debugLevel?: TSESTreeOptions['debugLevel']; filePath?: string; loc?: boolean; noWatch?: boolean; diff --git a/packages/experimental-utils/tests/eslint-utils/applyDefault.test.ts b/packages/experimental-utils/tests/eslint-utils/applyDefault.test.ts index 3ff4a843ec2e..c3304ec48714 100644 --- a/packages/experimental-utils/tests/eslint-utils/applyDefault.test.ts +++ b/packages/experimental-utils/tests/eslint-utils/applyDefault.test.ts @@ -1,23 +1,17 @@ -import assert from 'assert'; - import * as util from '../../src/eslint-utils/applyDefault'; describe('applyDefault', () => { it('returns a clone of the default if no options given', () => { - const defaults = [ - { - prop: 'setting', - }, - ]; + const defaults = [{ prop: 'setting' }]; const user = null; const result = util.applyDefault(defaults, user); - assert.deepStrictEqual(result, defaults); - assert.notStrictEqual(result, defaults); + expect(result).toStrictEqual(defaults); + expect(result).not.toBe(defaults); }); it('returns applies a deepMerge to each element in the array', () => { - const defaults = [ + const defaults: Record[] = [ { prop: 'setting1', other: 'other', @@ -25,16 +19,16 @@ describe('applyDefault', () => { { prop: 'setting2', }, - ] as Record[]; - const user = [ + ]; + const user: Record[] = [ { prop: 'new', other: 'something', }, - ] as Record[]; + ]; const result = util.applyDefault(defaults, user); - assert.deepStrictEqual(result, [ + expect(result).toStrictEqual([ { prop: 'new', other: 'something', @@ -43,8 +37,8 @@ describe('applyDefault', () => { prop: 'setting2', }, ]); - assert.notStrictEqual(result, defaults); - assert.notStrictEqual(result, user); + expect(result).not.toBe(defaults); + expect(result).not.toBe(user); }); it('returns a brand new array', () => { @@ -52,7 +46,7 @@ describe('applyDefault', () => { const user: undefined[] = []; const result = util.applyDefault(defaults, user); - assert.notStrictEqual(result, defaults); - assert.notStrictEqual(result, user); + expect(result).not.toBe(defaults); + expect(result).not.toBe(user); }); }); diff --git a/packages/experimental-utils/tests/eslint-utils/deepMerge.test.ts b/packages/experimental-utils/tests/eslint-utils/deepMerge.test.ts index 27e55c996a92..7caee854d492 100644 --- a/packages/experimental-utils/tests/eslint-utils/deepMerge.test.ts +++ b/packages/experimental-utils/tests/eslint-utils/deepMerge.test.ts @@ -1,5 +1,3 @@ -import assert from 'assert'; - import * as util from '../../src/eslint-utils/deepMerge'; describe('deepMerge', () => { @@ -8,8 +6,8 @@ describe('deepMerge', () => { const b = {}; const result = util.deepMerge(a, b); - assert.notStrictEqual(result, a); - assert.notStrictEqual(result, b); + expect(result).not.toBe(a); + expect(result).not.toBe(b); }); it('deeply merges objects', () => { @@ -40,7 +38,7 @@ describe('deepMerge', () => { }, }; - assert.deepStrictEqual(util.deepMerge(a, b), Object.assign({}, a, b)); + expect(util.deepMerge(a, b)).toStrictEqual(Object.assign({}, a, b)); }); it('deeply overwrites properties in the first one with the second', () => { @@ -55,6 +53,6 @@ describe('deepMerge', () => { }, }; - assert.deepStrictEqual(util.deepMerge(a, b), b); + expect(util.deepMerge(a, b)).toStrictEqual(b); }); }); diff --git a/packages/parser/CHANGELOG.md b/packages/parser/CHANGELOG.md index dddf4aacbc38..9655080e4d30 100644 --- a/packages/parser/CHANGELOG.md +++ b/packages/parser/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. +# [2.16.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.15.0...v2.16.0) (2020-01-13) + + +### Bug Fixes + +* **typescript-estree:** resolve path relative to tsconfigRootDir ([#1439](https://github.com/typescript-eslint/typescript-eslint/issues/1439)) ([c709056](https://github.com/typescript-eslint/typescript-eslint/commit/c709056)) + + + + + # [2.15.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.14.0...v2.15.0) (2020-01-06) **Note:** Version bump only for package @typescript-eslint/parser diff --git a/packages/parser/package.json b/packages/parser/package.json index 5827fb888ed5..c41abe25dd5e 100644 --- a/packages/parser/package.json +++ b/packages/parser/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/parser", - "version": "2.15.0", + "version": "2.16.0", "description": "An ESLint custom parser which leverages TypeScript ESTree", "main": "dist/parser.js", "types": "dist/parser.d.ts", @@ -43,13 +43,13 @@ }, "dependencies": { "@types/eslint-visitor-keys": "^1.0.0", - "@typescript-eslint/experimental-utils": "2.15.0", - "@typescript-eslint/typescript-estree": "2.15.0", + "@typescript-eslint/experimental-utils": "2.16.0", + "@typescript-eslint/typescript-estree": "2.16.0", "eslint-visitor-keys": "^1.1.0" }, "devDependencies": { "@types/glob": "^7.1.1", - "@typescript-eslint/shared-fixtures": "2.15.0", + "@typescript-eslint/shared-fixtures": "2.16.0", "glob": "*" }, "peerDependenciesMeta": { diff --git a/packages/parser/tests/lib/parser.ts b/packages/parser/tests/lib/parser.ts index 4f5136b036bc..56ce08b67862 100644 --- a/packages/parser/tests/lib/parser.ts +++ b/packages/parser/tests/lib/parser.ts @@ -50,7 +50,7 @@ describe('parser', () => { jsx: false, }, // ts-estree specific - filePath: 'tests/fixtures/services/isolated-file.src.ts', + filePath: 'isolated-file.src.ts', project: 'tsconfig.json', useJSXTextNode: false, errorOnUnknownASTType: false, diff --git a/packages/parser/tests/lib/services.ts b/packages/parser/tests/lib/services.ts index 40dabd2e6df1..37ddf0fadcf4 100644 --- a/packages/parser/tests/lib/services.ts +++ b/packages/parser/tests/lib/services.ts @@ -12,7 +12,9 @@ import { //------------------------------------------------------------------------------ const FIXTURES_DIR = './tests/fixtures/services'; -const testFiles = glob.sync(`${FIXTURES_DIR}/**/*.src.ts`); +const testFiles = glob.sync(`**/*.src.ts`, { + cwd: FIXTURES_DIR, +}); function createConfig(filename: string): object { return { @@ -29,7 +31,7 @@ function createConfig(filename: string): object { describe('services', () => { testFiles.forEach(filename => { - const code = fs.readFileSync(filename, 'utf8'); + const code = fs.readFileSync(path.join(FIXTURES_DIR, filename), 'utf8'); const config = createConfig(filename); it( formatSnapshotName(filename, FIXTURES_DIR, '.ts'), diff --git a/packages/shared-fixtures/CHANGELOG.md b/packages/shared-fixtures/CHANGELOG.md index ecdfa9ec099e..df6ff2f9c8b7 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. +# [2.16.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.15.0...v2.16.0) (2020-01-13) + +**Note:** Version bump only for package @typescript-eslint/shared-fixtures + + + + + # [2.15.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.14.0...v2.15.0) (2020-01-06) **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 ab7559eba6a5..8ce0c765b691 100644 --- a/packages/shared-fixtures/package.json +++ b/packages/shared-fixtures/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/shared-fixtures", - "version": "2.15.0", + "version": "2.16.0", "private": true, "scripts": { "build": "tsc -b tsconfig.build.json", diff --git a/packages/typescript-estree/CHANGELOG.md b/packages/typescript-estree/CHANGELOG.md index 28755f572ec7..df1e3a83cc90 100644 --- a/packages/typescript-estree/CHANGELOG.md +++ b/packages/typescript-estree/CHANGELOG.md @@ -3,6 +3,25 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.16.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.15.0...v2.16.0) (2020-01-13) + + +### Bug Fixes + +* **typescript-estree:** fix persisted parse for relative paths ([#1424](https://github.com/typescript-eslint/typescript-eslint/issues/1424)) ([9720d2c](https://github.com/typescript-eslint/typescript-eslint/commit/9720d2c)) +* **typescript-estree:** parsing of deeply nested new files in new folder ([#1412](https://github.com/typescript-eslint/typescript-eslint/issues/1412)) ([206c94b](https://github.com/typescript-eslint/typescript-eslint/commit/206c94b)) +* **typescript-estree:** resolve path relative to tsconfigRootDir ([#1439](https://github.com/typescript-eslint/typescript-eslint/issues/1439)) ([c709056](https://github.com/typescript-eslint/typescript-eslint/commit/c709056)) + + +### Features + +* **typescript-estree:** add parserOption to turn on debug logs ([#1413](https://github.com/typescript-eslint/typescript-eslint/issues/1413)) ([25092fd](https://github.com/typescript-eslint/typescript-eslint/commit/25092fd)) +* **typescript-estree:** add strict type mapping to esTreeNodeToTSNodeMap ([#1382](https://github.com/typescript-eslint/typescript-eslint/issues/1382)) ([d3d70a3](https://github.com/typescript-eslint/typescript-eslint/commit/d3d70a3)) + + + + + # [2.15.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.14.0...v2.15.0) (2020-01-06) diff --git a/packages/typescript-estree/README.md b/packages/typescript-estree/README.md index bd61d41e59d9..1d9a38d0fb34 100644 --- a/packages/typescript-estree/README.md +++ b/packages/typescript-estree/README.md @@ -29,36 +29,78 @@ yarn add -D @typescript-eslint/typescript-estree ## API -### parse(code, options) +### Parsing -Parses the given string of code with the options provided and returns an ESTree-compatible AST. The options object has the following properties: +#### `parse(code, options)` -```js -{ - // attach range information to each node - range: false, +Parses the given string of code with the options provided and returns an ESTree-compatible AST. - // attach line/column location information to each node - loc: false, +```ts +interface ParseOptions { + /** + * create a top-level comments array containing all comments + */ + comment?: boolean; - // create a top-level tokens array containing all tokens - tokens: false, + /** + * An array of modules to turn explicit debugging on for. + * - 'typescript-eslint' is the same as setting the env var `DEBUG=typescript-eslint:*` + * - 'eslint' is the same as setting the env var `DEBUG=eslint:*` + * - 'typescript' is the same as setting `extendedDiagnostics: true` in your tsconfig compilerOptions + * + * For convenience, also supports a boolean: + * - true === ['typescript-eslint'] + * - false === [] + */ + debugLevel?: boolean | ('typescript-eslint' | 'eslint' | 'typescript')[]; - // create a top-level comments array containing all comments - comment: false, + /** + * Cause the parser to error if it encounters an unknown AST node type (useful for testing). + * This case only usually occurs when TypeScript releases new features. + */ + errorOnUnknownASTType?: boolean; - /* - * enable parsing JSX. For more details, see https://www.typescriptlang.org/docs/handbook/jsx.html + /** + * The absolute path to the file being parsed. + */ + filePath?: string; + + /** + * Enable parsing of JSX. + * For more details, see https://www.typescriptlang.org/docs/handbook/jsx.html * * NOTE: this setting does not effect known file types (.js, .jsx, .ts, .tsx, .json) because the * TypeScript compiler has its own internal handling for known file extensions. * - * Exact behaviour: - * - .js, .jsx, .tsx files are parsed as if this is true - * - .ts files are parsed as if this is false - * - unknown extensions (.md, .vue) will respect this setting + * For the exact behavior, see https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/parser#parseroptionsecmafeaturesjsx */ - jsx: false, + jsx?: boolean; + + /** + * Controls whether the `loc` information to each node. + * The `loc` property is an object which contains the exact line/column the node starts/ends on. + * This is similar to the `range` property, except it is line/column relative. + */ + loc?: boolean; + + /* + * Allows overriding of function used for logging. + * When value is `false`, no logging will occur. + * When value is not provided, `console.log()` will be used. + */ + loggerFn?: Function | false; + + /** + * Controls whether the `range` property is included on AST nodes. + * The `range` property is a [number, number] which indicates the start/end index of the node in the file contents. + * This is similar to the `loc` property, except this is the absolute index. + */ + range?: boolean; + + /** + * Set to true to create a top-level array containing all tokens from the file. + */ + tokens?: boolean; /* * The JSX AST changed the node type for string literals @@ -66,17 +108,61 @@ Parses the given string of code with the options provided and returns an ESTree- * When value is `true`, these nodes will be parsed as type `JSXText`. * When value is `false`, these nodes will be parsed as type `Literal`. */ - useJSXTextNode: false, + useJSXTextNode?: boolean; +} - // Cause the parser to error if it encounters an unknown AST node type (useful for testing) +const PARSE_DEFAULT_OPTIONS: ParseOptions = { + comment: false, errorOnUnknownASTType: false, + filePath: 'estree.ts', // or 'estree.tsx', if you pass jsx: true + jsx: false, + loc: false, + loggerFn: undefined, + range: false, + tokens: false, + useJSXTextNode: false, +}; - /* - * Allows overriding of function used for logging. - * When value is `false`, no logging will occur. - * When value is not provided, `console.log()` will be used. +declare function parse( + code: string, + options: ParseOptions = PARSE_DEFAULT_OPTIONS, +): TSESTree.Program; +``` + +Example usage: + +```js +import { parse } from '@typescript-eslint/typescript-estree'; + +const code = `const hello: string = 'world';`; +const ast = parse(code, { + loc: true, + range: true, +}); +``` + +#### `parseAndGenerateServices(code, options)` + +Parses the given string of code with the options provided and returns an ESTree-compatible AST. Accepts additional options which can be used to generate type information along with the AST. + +```ts +interface ParseAndGenerateServicesOptions extends ParseOptions { + /** + * Causes the parser to error if the TypeScript compiler returns any unexpected syntax/semantic errors. */ - loggerFn: undefined, + errorOnTypeScriptSyntacticAndSemanticIssues?: boolean; + + /** + * When `project` is provided, this controls the non-standard file extensions which will be parsed. + * It accepts an array of file extensions, each preceded by a `.`. + */ + extraFileExtensions?: string[]; + + /** + * The absolute path to the file being parsed. + * When `project` is provided, this is required, as it is used to fetch the file from the TypeScript compiler's cache. + */ + filePath?: string; /** * Allows the user to control whether or not two-way AST node maps are preserved @@ -88,42 +174,67 @@ Parses the given string of code with the options provided and returns an ESTree- * NOTE: If `preserveNodeMaps` is explicitly set by the user, it will be respected, * regardless of whether or not `project` is in use. */ - preserveNodeMaps: undefined + preserveNodeMaps?: boolean; + + /** + * Absolute (or relative to `tsconfigRootDir`) paths to the tsconfig(s). + * If this is provided, type information will be returned. + */ + project?: string | string[]; + + /** + * The absolute path to the root directory for all provided `project`s. + */ + tsconfigRootDir?: string; + + /** + *************************************************************************************** + * IT IS RECOMMENDED THAT YOU DO NOT USE THIS OPTION, AS IT CAUSES PERFORMANCE ISSUES. * + *************************************************************************************** + * + * When passed with `project`, this allows the parser to create a catch-all, default program. + * This means that if the parser encounters a file not included in any of the provided `project`s, + * it will not error, but will instead parse the file and its dependencies in a new program. + */ + createDefaultProgram?: boolean; } + +const PARSE_AND_GENERATE_SERVICES_DEFAULT_OPTIONS: ParseOptions = { + ...PARSE_DEFAULT_OPTIONS, + errorOnTypeScriptSyntacticAndSemanticIssues: false, + extraFileExtensions: [], + preserveNodeMaps: false, // or true, if you do not set this, but pass `project` + project: undefined, + tsconfigRootDir: process.cwd(), +}; + +declare function parseAndGenerateServices( + code: string, + options: ParseOptions = PARSE_DEFAULT_OPTIONS, +): TSESTree.Program; ``` Example usage: ```js -const parser = require('@typescript-eslint/typescript-estree'); +import { parseAndGenerateServices } from '@typescript-eslint/typescript-estree'; + const code = `const hello: string = 'world';`; -const ast = parser.parse(code, { - range: true, +const ast = parseAndGenerateServices(code, { + filePath: '/some/path/to/file/foo.ts', loc: true, + project: './tsconfig.json', + range: true, }); ``` -### version +### `TSESTree`, `AST_NODE_TYPES` and `AST_TOKEN_TYPES` -Exposes the current version of `typescript-estree` as specified in `package.json`. +Types for the AST produced by the parse functions. -Example usage: - -```js -const parser = require('@typescript-eslint/typescript-estree'); -const version = parser.version; -``` - -### `AST_NODE_TYPES` - -Exposes an object that contains the AST node types produced by the parser. - -Example usage: - -```js -const parser = require('@typescript-eslint/typescript-estree'); -const astNodeTypes = parser.AST_NODE_TYPES; -``` +- `TSESTree` is a namespace which contains object types representing all of the AST Nodes produced by the parser. +- `AST_NODE_TYPES` is an enum which provides the values for every single AST node's `type` property. +- `AST_TOKEN_TYPES` is an enum which provides the values for every single AST token's `type` property. ## Supported TypeScript Version diff --git a/packages/typescript-estree/package.json b/packages/typescript-estree/package.json index 2c68153bfac4..ed9b62857559 100644 --- a/packages/typescript-estree/package.json +++ b/packages/typescript-estree/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/typescript-estree", - "version": "2.15.0", + "version": "2.16.0", "description": "A parser that converts TypeScript source code into an ESTree compatible form", "main": "dist/parser.js", "types": "dist/parser.d.ts", @@ -43,22 +43,22 @@ "eslint-visitor-keys": "^1.1.0", "glob": "^7.1.6", "is-glob": "^4.0.1", - "lodash.unescape": "4.0.1", + "lodash": "^4.17.15", "semver": "^6.3.0", "tsutils": "^3.17.1" }, "devDependencies": { "@babel/code-frame": "7.5.5", - "@babel/parser": "7.7.5", + "@babel/parser": "7.7.7", "@babel/types": "^7.7.4", "@types/babel__code-frame": "^7.0.1", "@types/debug": "^4.1.5", "@types/glob": "^7.1.1", "@types/is-glob": "^4.0.1", - "@types/lodash.unescape": "^4.0.4", + "@types/lodash": "^4.14.149", "@types/semver": "^6.2.0", "@types/tmp": "^0.1.0", - "@typescript-eslint/shared-fixtures": "2.15.0", + "@typescript-eslint/shared-fixtures": "2.16.0", "tmp": "^0.1.0", "typescript": "*" }, diff --git a/packages/typescript-estree/src/convert.ts b/packages/typescript-estree/src/convert.ts index 4a5d4accb408..1f079b710552 100644 --- a/packages/typescript-estree/src/convert.ts +++ b/packages/typescript-estree/src/convert.ts @@ -21,8 +21,13 @@ import { unescapeStringLiteralText, TSError, } from './node-utils'; -import { AST_NODE_TYPES, TSESTree, TSNode } from './ts-estree'; -import { ParserWeakMap } from './parser-options'; +import { + AST_NODE_TYPES, + TSESTree, + TSNode, + TSESTreeToTSNode, +} from './ts-estree'; +import { ParserWeakMap, ParserWeakMapESTreeToTSNode } from './parser-options'; const SyntaxKind = ts.SyntaxKind; @@ -46,7 +51,7 @@ export function convertError(error: any): TSError { } export interface ASTMaps { - esTreeNodeToTSNodeMap: ParserWeakMap; + esTreeNodeToTSNodeMap: ParserWeakMapESTreeToTSNode; tsNodeToESTreeNodeMap: ParserWeakMap; } @@ -125,12 +130,20 @@ export class Converter { /** * Fixes the exports of the given ts.Node - * @param node the ts.Node + * @param node the ts.Node * @param result result * @returns the ESTreeNode with fixed exports */ private fixExports( - node: ts.Node, + node: + | ts.FunctionDeclaration + | ts.VariableStatement + | ts.ClassDeclaration + | ts.ClassExpression + | ts.TypeAliasDeclaration + | ts.InterfaceDeclaration + | ts.EnumDeclaration + | ts.ModuleDeclaration, result: T, ): TSESTree.ExportDefaultDeclaration | TSESTree.ExportNamedDeclaration | T { // check for exports @@ -216,8 +229,8 @@ export class Converter { return this.converter(child, parent, true, false); } - private createNode( - node: ts.Node, + private createNode( + node: TSESTreeToTSNode, data: TSESTree.OptionalRangeAndLoc, ): T { const result = data; @@ -306,7 +319,7 @@ export class Converter { */ private convertTypeArgumentsToTypeParameters( typeArguments: ts.NodeArray, - node: ts.Node, + node: TSESTreeToTSNode, ): TSESTree.TSTypeParameterInstantiation { const greaterThanToken = findNextToken(typeArguments, this.ast, this.ast)!; @@ -1821,10 +1834,14 @@ export class Converter { case SyntaxKind.MetaProperty: { return this.createNode(node, { type: AST_NODE_TYPES.MetaProperty, - meta: this.createNode(node.getFirstToken()!, { - type: AST_NODE_TYPES.Identifier, - name: getTextForTokenKind(node.keywordToken), - }), + meta: this.createNode( + // TODO: do we really want to convert it to Token? + node.getFirstToken()! as ts.Token, + { + type: AST_NODE_TYPES.Identifier, + name: getTextForTokenKind(node.keywordToken), + }, + ), property: this.convertChild(node.name), }); } @@ -1914,7 +1931,7 @@ export class Converter { type: AST_NODE_TYPES.TSNullKeyword, }); } else { - return this.createNode(node, { + return this.createNode(node as ts.NullLiteral, { type: AST_NODE_TYPES.Literal, value: null, raw: 'null', @@ -2114,6 +2131,10 @@ export class Converter { } case SyntaxKind.ThisType: + return this.createNode(node, { + type: AST_NODE_TYPES.TSThisType, + }); + case SyntaxKind.AnyKeyword: case SyntaxKind.BigIntKeyword: case SyntaxKind.BooleanKeyword: diff --git a/packages/typescript-estree/src/create-program/createDefaultProgram.ts b/packages/typescript-estree/src/create-program/createDefaultProgram.ts index 0db0d1fc699d..c1e69b253350 100644 --- a/packages/typescript-estree/src/create-program/createDefaultProgram.ts +++ b/packages/typescript-estree/src/create-program/createDefaultProgram.ts @@ -3,9 +3,9 @@ import path from 'path'; import * as ts from 'typescript'; import { Extra } from '../parser-options'; import { - getTsconfigPath, - DEFAULT_COMPILER_OPTIONS, ASTAndProgram, + getTsconfigPath, + createDefaultCompilerOptionsFromExtra, } from './shared'; const log = debug('typescript-eslint:typescript-estree:createDefaultProgram'); @@ -31,7 +31,7 @@ function createDefaultProgram( const commandLine = ts.getParsedCommandLineOfConfigFile( tsconfigPath, - DEFAULT_COMPILER_OPTIONS, + createDefaultCompilerOptionsFromExtra(extra), { ...ts.sys, onUnRecoverableConfigFileDiagnostic: () => {} }, ); diff --git a/packages/typescript-estree/src/create-program/createIsolatedProgram.ts b/packages/typescript-estree/src/create-program/createIsolatedProgram.ts index c6c74b8c5ab9..257577d5eb2e 100644 --- a/packages/typescript-estree/src/create-program/createIsolatedProgram.ts +++ b/packages/typescript-estree/src/create-program/createIsolatedProgram.ts @@ -3,7 +3,7 @@ import * as ts from 'typescript'; import { Extra } from '../parser-options'; import { ASTAndProgram, - DEFAULT_COMPILER_OPTIONS, + createDefaultCompilerOptionsFromExtra, getScriptKind, } from './shared'; @@ -67,7 +67,7 @@ function createIsolatedProgram(code: string, extra: Extra): ASTAndProgram { noResolve: true, target: ts.ScriptTarget.Latest, jsx: extra.jsx ? ts.JsxEmit.Preserve : undefined, - ...DEFAULT_COMPILER_OPTIONS, + ...createDefaultCompilerOptionsFromExtra(extra), }, compilerHost, ); diff --git a/packages/typescript-estree/src/create-program/createProjectProgram.ts b/packages/typescript-estree/src/create-program/createProjectProgram.ts index 32f81a40f667..b81409e647e6 100644 --- a/packages/typescript-estree/src/create-program/createProjectProgram.ts +++ b/packages/typescript-estree/src/create-program/createProjectProgram.ts @@ -35,7 +35,7 @@ function createProjectProgram( const errorLines = [ '"parserOptions.project" has been set for @typescript-eslint/parser.', `The file does not match your project config: ${path.relative( - process.cwd(), + extra.tsconfigRootDir || process.cwd(), extra.filePath, )}.`, ]; diff --git a/packages/typescript-estree/src/create-program/createWatchProgram.ts b/packages/typescript-estree/src/create-program/createWatchProgram.ts index 72b5535c5783..dd4401af3b60 100644 --- a/packages/typescript-estree/src/create-program/createWatchProgram.ts +++ b/packages/typescript-estree/src/create-program/createWatchProgram.ts @@ -6,9 +6,9 @@ import { WatchCompilerHostOfConfigFile } from './WatchCompilerHostOfConfigFile'; import { canonicalDirname, CanonicalPath, - getTsconfigPath, - DEFAULT_COMPILER_OPTIONS, + createDefaultCompilerOptionsFromExtra, getCanonicalFileName, + getTsconfigPath, } from './shared'; const log = debug('typescript-eslint:typescript-estree:createWatchProgram'); @@ -233,7 +233,7 @@ function createWatchProgram( // create compiler host const watchCompilerHost = ts.createWatchCompilerHost( tsconfigPath, - DEFAULT_COMPILER_OPTIONS, + createDefaultCompilerOptionsFromExtra(extra), ts.sys, ts.createSemanticDiagnosticsBuilderProgram, diagnosticReporter, @@ -394,11 +394,13 @@ function maybeInvalidateProgram( current = next; const folderWatchCallbacks = folderWatchCallbackTrackingMap.get(current); if (folderWatchCallbacks) { - folderWatchCallbacks.forEach(cb => - cb(currentDir, ts.FileWatcherEventKind.Changed), - ); + folderWatchCallbacks.forEach(cb => { + if (currentDir !== current) { + cb(currentDir, ts.FileWatcherEventKind.Changed); + } + cb(current!, ts.FileWatcherEventKind.Changed); + }); hasCallback = true; - break; } next = canonicalDirname(current); diff --git a/packages/typescript-estree/src/create-program/shared.ts b/packages/typescript-estree/src/create-program/shared.ts index 8aceb204c474..1aa6a4fe3c0c 100644 --- a/packages/typescript-estree/src/create-program/shared.ts +++ b/packages/typescript-estree/src/create-program/shared.ts @@ -20,6 +20,19 @@ const DEFAULT_COMPILER_OPTIONS: ts.CompilerOptions = { noUnusedParameters: true, }; +function createDefaultCompilerOptionsFromExtra( + extra: Extra, +): ts.CompilerOptions { + if (extra.debugLevel.has('typescript')) { + return { + ...DEFAULT_COMPILER_OPTIONS, + extendedDiagnostics: true, + }; + } + + return DEFAULT_COMPILER_OPTIONS; +} + // This narrows the type so we can be sure we're passing canonical names in the correct places type CanonicalPath = string & { __brand: unknown }; @@ -38,12 +51,14 @@ function getCanonicalFileName(filePath: string): CanonicalPath { return correctPathCasing(normalized) as CanonicalPath; } +function ensureAbsolutePath(p: string, extra: Extra): string { + return path.isAbsolute(p) + ? p + : path.join(extra.tsconfigRootDir || process.cwd(), p); +} + function getTsconfigPath(tsconfigPath: string, extra: Extra): CanonicalPath { - return getCanonicalFileName( - path.isAbsolute(tsconfigPath) - ? tsconfigPath - : path.join(extra.tsconfigRootDir || process.cwd(), tsconfigPath), - ); + return getCanonicalFileName(ensureAbsolutePath(tsconfigPath, extra)); } function canonicalDirname(p: CanonicalPath): CanonicalPath { @@ -83,7 +98,8 @@ export { ASTAndProgram, canonicalDirname, CanonicalPath, - DEFAULT_COMPILER_OPTIONS, + createDefaultCompilerOptionsFromExtra, + ensureAbsolutePath, getCanonicalFileName, getScriptKind, getTsconfigPath, diff --git a/packages/typescript-estree/src/node-utils.ts b/packages/typescript-estree/src/node-utils.ts index 1995bbf57c56..77ffca87cc47 100644 --- a/packages/typescript-estree/src/node-utils.ts +++ b/packages/typescript-estree/src/node-utils.ts @@ -1,4 +1,4 @@ -import unescape from 'lodash.unescape'; +import unescape from 'lodash/unescape'; import * as ts from 'typescript'; import { AST_NODE_TYPES, AST_TOKEN_TYPES, TSESTree } from './ts-estree'; diff --git a/packages/typescript-estree/src/parser-options.ts b/packages/typescript-estree/src/parser-options.ts index 63e14a2fffb1..1cd8b4d54750 100644 --- a/packages/typescript-estree/src/parser-options.ts +++ b/packages/typescript-estree/src/parser-options.ts @@ -1,11 +1,14 @@ import { Program } from 'typescript'; -import { TSESTree, TSNode } from './ts-estree'; +import { TSESTree, TSNode, TSESTreeToTSNode, TSToken } from './ts-estree'; + +type DebugModule = 'typescript-eslint' | 'eslint' | 'typescript'; export interface Extra { code: string; comment: boolean; comments: TSESTree.Comment[]; createDefaultProgram: boolean; + debugLevel: Set; errorOnTypeScriptSyntacticAndSemanticIssues: boolean; errorOnUnknownASTType: boolean; extraFileExtensions: string[]; @@ -22,22 +25,131 @@ export interface Extra { useJSXTextNode: boolean; } +//////////////////////////////////////////////////// +// MAKE SURE THIS IS KEPT IN SYNC WITH THE README // +//////////////////////////////////////////////////// + export interface TSESTreeOptions { + /** + * create a top-level comments array containing all comments + */ comment?: boolean; - createDefaultProgram?: boolean; + + /** + * For convenience: + * - true === ['typescript-eslint'] + * - false === [] + * + * An array of modules to turn explicit debugging on for. + * - 'typescript-eslint' is the same as setting the env var `DEBUG=typescript-eslint:*` + * - 'eslint' is the same as setting the env var `DEBUG=eslint:*` + * - 'typescript' is the same as setting `extendedDiagnostics: true` in your tsconfig compilerOptions + */ + debugLevel?: boolean | DebugModule[]; + + /** + * Causes the parser to error if the TypeScript compiler returns any unexpected syntax/semantic errors. + */ errorOnTypeScriptSyntacticAndSemanticIssues?: boolean; + + /** + * Cause the parser to error if it encounters an unknown AST node type (useful for testing). + * This case only usually occurs when TypeScript releases new features. + */ errorOnUnknownASTType?: boolean; + + /** + * When `project` is provided, this controls the non-standard file extensions which will be parsed. + * It accepts an array of file extensions, each preceded by a `.`. + */ extraFileExtensions?: string[]; + + /** + * The absolute path to the file being parsed. + * When `project` is provided, this is required, as it is used to fetch the file from the TypeScript compiler's cache. + */ filePath?: string; + + /** + * Enable parsing of JSX. + * For more details, see https://www.typescriptlang.org/docs/handbook/jsx.html + * + * NOTE: this setting does not effect known file types (.js, .jsx, .ts, .tsx, .json) because the + * TypeScript compiler has its own internal handling for known file extensions. + * + * For the exact behavior, see https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/parser#parseroptionsecmafeaturesjsx + */ jsx?: boolean; + + /** + * Controls whether the `loc` information to each node. + * The `loc` property is an object which contains the exact line/column the node starts/ends on. + * This is similar to the `range` property, except it is line/column relative. + */ loc?: boolean; + + /* + * Allows overriding of function used for logging. + * When value is `false`, no logging will occur. + * When value is not provided, `console.log()` will be used. + */ loggerFn?: Function | false; + + /** + * Allows the user to control whether or not two-way AST node maps are preserved + * during the AST conversion process. + * + * By default: the AST node maps are NOT preserved, unless `project` has been specified, + * in which case the maps are made available on the returned `parserServices`. + * + * NOTE: If `preserveNodeMaps` is explicitly set by the user, it will be respected, + * regardless of whether or not `project` is in use. + */ preserveNodeMaps?: boolean; + + /** + * Absolute (or relative to `tsconfigRootDir`) paths to the tsconfig(s). + * If this is provided, type information will be returned. + */ project?: string | string[]; + + /** + * Controls whether the `range` property is included on AST nodes. + * The `range` property is a [number, number] which indicates the start/end index of the node in the file contents. + * This is similar to the `loc` property, except this is the absolute index. + */ range?: boolean; + + /** + * Set to true to create a top-level array containing all tokens from the file. + */ tokens?: boolean; + + /** + * The absolute path to the root directory for all provided `project`s. + */ tsconfigRootDir?: string; + + /* + * The JSX AST changed the node type for string literals + * inside a JSX Element from `Literal` to `JSXText`. + * When value is `true`, these nodes will be parsed as type `JSXText`. + * When value is `false`, these nodes will be parsed as type `Literal`. + */ useJSXTextNode?: boolean; + + /** + *************************************************************************************** + * IT IS RECOMMENDED THAT YOU DO NOT USE THIS OPTION, AS IT CAUSES PERFORMANCE ISSUES. * + *************************************************************************************** + * + * When passed with `project`, this allows the parser to create a catch-all, default program. + * This means that if the parser encounters a file not included in any of the provided `project`s, + * it will not error, but will instead parse the file and its dependencies in a new program. + * + * This + */ + createDefaultProgram?: boolean; } // This lets us use generics to type the return value, and removes the need to @@ -47,8 +159,17 @@ export interface ParserWeakMap { has(key: unknown): boolean; } +export interface ParserWeakMapESTreeToTSNode< + TKey extends TSESTree.Node = TSESTree.Node +> { + get(key: TKeyBase): TSESTreeToTSNode; + has(key: unknown): boolean; +} + export interface ParserServices { program: Program | undefined; - esTreeNodeToTSNodeMap: ParserWeakMap | undefined; - tsNodeToESTreeNodeMap: ParserWeakMap | undefined; + esTreeNodeToTSNodeMap: ParserWeakMapESTreeToTSNode | undefined; + tsNodeToESTreeNodeMap: + | ParserWeakMap + | undefined; } diff --git a/packages/typescript-estree/src/parser.ts b/packages/typescript-estree/src/parser.ts index fb62c090bb79..6142efaf1af2 100644 --- a/packages/typescript-estree/src/parser.ts +++ b/packages/typescript-estree/src/parser.ts @@ -1,7 +1,8 @@ -import semver from 'semver'; -import * as ts from 'typescript'; +import debug from 'debug'; import { sync as globSync } from 'glob'; import isGlob from 'is-glob'; +import semver from 'semver'; +import * as ts from 'typescript'; import { astConverter } from './ast-converter'; import { convertError } from './convert'; import { createDefaultProgram } from './create-program/createDefaultProgram'; @@ -11,6 +12,7 @@ import { createSourceFile } from './create-program/createSourceFile'; import { Extra, TSESTreeOptions, ParserServices } from './parser-options'; import { getFirstSemanticOrSyntacticError } from './semantic-or-syntactic-errors'; import { TSESTree } from './ts-estree'; +import { ensureAbsolutePath } from './create-program/shared'; /** * This needs to be kept in sync with the top-level README.md in the @@ -91,6 +93,7 @@ function resetExtra(): void { comment: false, comments: [], createDefaultProgram: false, + debugLevel: new Set(), errorOnTypeScriptSyntacticAndSemanticIssues: false, errorOnUnknownASTType: false, extraFileExtensions: [], @@ -109,6 +112,31 @@ function resetExtra(): void { } function applyParserOptionsToExtra(options: TSESTreeOptions): void { + /** + * Configure Debug logging + */ + if (options.debugLevel === true) { + extra.debugLevel = new Set(['typescript-eslint']); + } else if (Array.isArray(options.debugLevel)) { + extra.debugLevel = new Set(options.debugLevel); + } + if (extra.debugLevel.size > 0) { + // debug doesn't support multiple `enable` calls, so have to do it all at once + const namespaces = []; + if (extra.debugLevel.has('typescript-eslint')) { + namespaces.push('typescript-eslint:*'); + } + if ( + extra.debugLevel.has('eslint') || + // make sure we don't turn off the eslint debug if it was enabled via --debug + debug.enabled('eslint:*') + ) { + // https://github.com/eslint/eslint/blob/9dfc8501fb1956c90dc11e6377b4cb38a6bea65d/bin/eslint.js#L25 + namespaces.push('eslint:*,-eslint:code-path'); + } + debug.enable(namespaces.join(',')); + } + /** * Track range information in the AST */ @@ -189,6 +217,7 @@ function applyParserOptionsToExtra(options: TSESTreeOptions): void { if (typeof options.tsconfigRootDir === 'string') { extra.tsconfigRootDir = options.tsconfigRootDir; } + extra.filePath = ensureAbsolutePath(extra.filePath, extra); // Transform glob patterns into paths if (extra.projects) { @@ -255,7 +284,6 @@ function warnAboutTSVersion(): void { //------------------------------------------------------------------------------ type AST = TSESTree.Program & - (T['range'] extends true ? { range: [number, number] } : {}) & (T['tokens'] extends true ? { tokens: TSESTree.Token[] } : {}) & (T['comment'] extends true ? { comments: TSESTree.Comment[] } : {}); @@ -415,10 +443,9 @@ export { parse, parseAndGenerateServices, ParseAndGenerateServicesResult, - ParserServices, - TSESTreeOptions, version, }; +export { ParserServices, TSESTreeOptions } from './parser-options'; export { simpleTraverse } from './simple-traverse'; export { visitorKeys } from './visitor-keys'; export * from './ts-estree'; diff --git a/packages/typescript-estree/src/ts-estree/estree-to-ts-node-types.ts b/packages/typescript-estree/src/ts-estree/estree-to-ts-node-types.ts new file mode 100644 index 000000000000..bb00046c3349 --- /dev/null +++ b/packages/typescript-estree/src/ts-estree/estree-to-ts-node-types.ts @@ -0,0 +1,279 @@ +import { TSNode } from './ts-nodes'; +import { AST_NODE_TYPES } from './ast-node-types'; +import { Node } from './ts-estree'; +import * as ts from 'typescript'; + +export interface EstreeToTsNodeTypes { + [AST_NODE_TYPES.ArrayExpression]: ts.ArrayLiteralExpression; + [AST_NODE_TYPES.ArrayPattern]: + | ts.ArrayLiteralExpression + | ts.ArrayBindingPattern; + [AST_NODE_TYPES.ArrowFunctionExpression]: ts.ArrowFunction; + [AST_NODE_TYPES.AssignmentExpression]: ts.BinaryExpression; + [AST_NODE_TYPES.AssignmentPattern]: + | ts.ShorthandPropertyAssignment + | ts.BindingElement + | ts.BinaryExpression + | ts.ParameterDeclaration; + [AST_NODE_TYPES.AwaitExpression]: ts.AwaitExpression; + [AST_NODE_TYPES.BigIntLiteral]: ts.BigIntLiteral; + [AST_NODE_TYPES.BinaryExpression]: ts.BinaryExpression; + [AST_NODE_TYPES.BlockStatement]: ts.Block; + [AST_NODE_TYPES.BreakStatement]: ts.BreakStatement; + [AST_NODE_TYPES.CallExpression]: ts.CallExpression; + [AST_NODE_TYPES.CatchClause]: ts.CatchClause; + [AST_NODE_TYPES.ClassBody]: ts.ClassDeclaration | ts.ClassExpression; + [AST_NODE_TYPES.ClassDeclaration]: ts.ClassDeclaration; + [AST_NODE_TYPES.ClassExpression]: ts.ClassExpression; + [AST_NODE_TYPES.ClassProperty]: ts.PropertyDeclaration; + [AST_NODE_TYPES.ConditionalExpression]: ts.ConditionalExpression; + [AST_NODE_TYPES.ContinueStatement]: ts.ContinueStatement; + [AST_NODE_TYPES.DebuggerStatement]: ts.DebuggerStatement; + [AST_NODE_TYPES.Decorator]: ts.Decorator; + [AST_NODE_TYPES.DoWhileStatement]: ts.DoStatement; + [AST_NODE_TYPES.EmptyStatement]: ts.EmptyStatement; + [AST_NODE_TYPES.ExportAllDeclaration]: ts.ExportDeclaration; + [AST_NODE_TYPES.ExportDefaultDeclaration]: + | ts.ExportAssignment + | ts.FunctionDeclaration + | ts.VariableStatement + | ts.ClassDeclaration + | ts.ClassExpression + | ts.TypeAliasDeclaration + | ts.InterfaceDeclaration + | ts.EnumDeclaration + | ts.ModuleDeclaration; + [AST_NODE_TYPES.ExportNamedDeclaration]: + | ts.ExportDeclaration + | ts.FunctionDeclaration + | ts.VariableStatement + | ts.ClassDeclaration + | ts.ClassExpression + | ts.TypeAliasDeclaration + | ts.InterfaceDeclaration + | ts.EnumDeclaration + | ts.ModuleDeclaration; + [AST_NODE_TYPES.ExportSpecifier]: ts.ExportSpecifier; + [AST_NODE_TYPES.ExpressionStatement]: ts.ExpressionStatement; + [AST_NODE_TYPES.ForInStatement]: ts.ForInStatement; + [AST_NODE_TYPES.ForOfStatement]: ts.ForOfStatement; + [AST_NODE_TYPES.ForStatement]: ts.ForStatement; + [AST_NODE_TYPES.FunctionDeclaration]: ts.FunctionDeclaration; + [AST_NODE_TYPES.FunctionExpression]: + | ts.FunctionExpression + | ts.ConstructorDeclaration + | ts.GetAccessorDeclaration + | ts.SetAccessorDeclaration + | ts.MethodDeclaration; + [AST_NODE_TYPES.Identifier]: + | ts.Identifier + | ts.ConstructorDeclaration + | ts.Token; + [AST_NODE_TYPES.IfStatement]: ts.IfStatement; + [AST_NODE_TYPES.Import]: ts.ImportExpression; + [AST_NODE_TYPES.ImportDeclaration]: ts.ImportDeclaration; + [AST_NODE_TYPES.ImportDefaultSpecifier]: ts.ImportClause; + [AST_NODE_TYPES.ImportNamespaceSpecifier]: ts.NamespaceImport; + [AST_NODE_TYPES.ImportSpecifier]: ts.ImportSpecifier; + [AST_NODE_TYPES.JSXAttribute]: ts.JsxAttribute; + [AST_NODE_TYPES.JSXClosingElement]: ts.JsxClosingElement; + [AST_NODE_TYPES.JSXClosingFragment]: ts.JsxClosingFragment; + [AST_NODE_TYPES.JSXElement]: ts.JsxElement | ts.JsxSelfClosingElement; + [AST_NODE_TYPES.JSXEmptyExpression]: ts.JsxExpression; + [AST_NODE_TYPES.JSXExpressionContainer]: ts.JsxExpression; + [AST_NODE_TYPES.JSXFragment]: ts.JsxFragment; + [AST_NODE_TYPES.JSXIdentifier]: ts.Identifier | ts.ThisExpression; + [AST_NODE_TYPES.JSXOpeningElement]: + | ts.JsxOpeningElement + | ts.JsxSelfClosingElement; + [AST_NODE_TYPES.JSXOpeningFragment]: ts.JsxOpeningFragment; + [AST_NODE_TYPES.JSXSpreadAttribute]: ts.JsxSpreadAttribute; + [AST_NODE_TYPES.JSXSpreadChild]: ts.JsxExpression; + [AST_NODE_TYPES.JSXMemberExpression]: ts.PropertyAccessExpression; + [AST_NODE_TYPES.JSXText]: ts.JsxText; + [AST_NODE_TYPES.LabeledStatement]: ts.LabeledStatement; + [AST_NODE_TYPES.Literal]: + | ts.StringLiteral + | ts.NumericLiteral + | ts.RegularExpressionLiteral + | ts.JsxText + | ts.NullLiteral + | ts.BooleanLiteral; + [AST_NODE_TYPES.LogicalExpression]: ts.BinaryExpression; + [AST_NODE_TYPES.MemberExpression]: + | ts.PropertyAccessExpression + | ts.ElementAccessExpression; + [AST_NODE_TYPES.MetaProperty]: ts.MetaProperty; + [AST_NODE_TYPES.MethodDefinition]: + | ts.GetAccessorDeclaration + | ts.SetAccessorDeclaration + | ts.MethodDeclaration + | ts.ConstructorDeclaration; + [AST_NODE_TYPES.NewExpression]: ts.NewExpression; + [AST_NODE_TYPES.ObjectExpression]: ts.ObjectLiteralExpression; + [AST_NODE_TYPES.ObjectPattern]: + | ts.ObjectLiteralExpression + | ts.ObjectBindingPattern; + [AST_NODE_TYPES.OptionalCallExpression]: ts.CallExpression; + [AST_NODE_TYPES.OptionalMemberExpression]: + | ts.PropertyAccessExpression + | ts.ElementAccessExpression; + [AST_NODE_TYPES.Program]: ts.SourceFile; + [AST_NODE_TYPES.Property]: + | ts.PropertyAssignment + | ts.ShorthandPropertyAssignment + | ts.GetAccessorDeclaration + | ts.SetAccessorDeclaration + | ts.MethodDeclaration + | ts.BindingElement; + [AST_NODE_TYPES.RestElement]: + | ts.BindingElement + | ts.SpreadAssignment + | ts.SpreadElement + | ts.ParameterDeclaration; + [AST_NODE_TYPES.ReturnStatement]: ts.ReturnStatement; + [AST_NODE_TYPES.SequenceExpression]: ts.BinaryExpression; + [AST_NODE_TYPES.SpreadElement]: ts.SpreadElement | ts.SpreadAssignment; + [AST_NODE_TYPES.Super]: ts.SuperExpression; + [AST_NODE_TYPES.SwitchCase]: ts.CaseClause | ts.DefaultClause; + [AST_NODE_TYPES.SwitchStatement]: ts.SwitchStatement; + [AST_NODE_TYPES.TaggedTemplateExpression]: ts.TaggedTemplateExpression; + [AST_NODE_TYPES.TemplateElement]: + | ts.NoSubstitutionTemplateLiteral + | ts.TemplateHead + | ts.TemplateMiddle + | ts.TemplateTail; + [AST_NODE_TYPES.TemplateLiteral]: + | ts.NoSubstitutionTemplateLiteral + | ts.TemplateExpression; + [AST_NODE_TYPES.ThisExpression]: ts.ThisExpression | ts.KeywordTypeNode; + [AST_NODE_TYPES.ThrowStatement]: ts.ThrowStatement; + [AST_NODE_TYPES.TryStatement]: ts.TryStatement; + [AST_NODE_TYPES.TSAbstractClassProperty]: ts.PropertyDeclaration; + [AST_NODE_TYPES.TSAbstractMethodDefinition]: + | ts.GetAccessorDeclaration + | ts.SetAccessorDeclaration + | ts.MethodDeclaration + | ts.ConstructorDeclaration; + [AST_NODE_TYPES.TSArrayType]: ts.ArrayTypeNode; + [AST_NODE_TYPES.TSAsExpression]: ts.AsExpression; + [AST_NODE_TYPES.TSCallSignatureDeclaration]: ts.PropertySignature; + [AST_NODE_TYPES.TSClassImplements]: ts.ExpressionWithTypeArguments; + [AST_NODE_TYPES.TSConditionalType]: ts.ConditionalTypeNode; + [AST_NODE_TYPES.TSConstructorType]: ts.ConstructorTypeNode; + [AST_NODE_TYPES.TSConstructSignatureDeclaration]: + | ts.ConstructorTypeNode + | ts.FunctionTypeNode + | ts.ConstructSignatureDeclaration + | ts.CallSignatureDeclaration; + [AST_NODE_TYPES.TSDeclareFunction]: ts.FunctionDeclaration; + [AST_NODE_TYPES.TSEnumDeclaration]: ts.EnumDeclaration; + [AST_NODE_TYPES.TSEnumMember]: ts.EnumMember; + [AST_NODE_TYPES.TSExportAssignment]: ts.ExportAssignment; + [AST_NODE_TYPES.TSExternalModuleReference]: ts.ExternalModuleReference; + [AST_NODE_TYPES.TSFunctionType]: ts.FunctionTypeNode; + [AST_NODE_TYPES.TSImportEqualsDeclaration]: ts.ImportEqualsDeclaration; + [AST_NODE_TYPES.TSImportType]: ts.ImportTypeNode; + [AST_NODE_TYPES.TSIndexedAccessType]: ts.IndexedAccessTypeNode; + [AST_NODE_TYPES.TSIndexSignature]: ts.IndexSignatureDeclaration; + [AST_NODE_TYPES.TSInferType]: ts.InferTypeNode; + [AST_NODE_TYPES.TSInterfaceDeclaration]: ts.InterfaceDeclaration; + [AST_NODE_TYPES.TSInterfaceBody]: ts.InterfaceDeclaration; + [AST_NODE_TYPES.TSInterfaceHeritage]: ts.ExpressionWithTypeArguments; + [AST_NODE_TYPES.TSIntersectionType]: ts.IntersectionTypeNode; + [AST_NODE_TYPES.TSLiteralType]: ts.LiteralTypeNode; + [AST_NODE_TYPES.TSMappedType]: ts.MappedTypeNode; + [AST_NODE_TYPES.TSMethodSignature]: ts.MethodSignature; + [AST_NODE_TYPES.TSModuleBlock]: ts.ModuleBlock; + [AST_NODE_TYPES.TSModuleDeclaration]: ts.ModuleDeclaration; + [AST_NODE_TYPES.TSNamespaceExportDeclaration]: ts.NamespaceExportDeclaration; + [AST_NODE_TYPES.TSNonNullExpression]: ts.NonNullExpression; + [AST_NODE_TYPES.TSOptionalType]: ts.OptionalTypeNode; + [AST_NODE_TYPES.TSParameterProperty]: ts.ParameterDeclaration; + [AST_NODE_TYPES.TSParenthesizedType]: ts.ParenthesizedTypeNode; + [AST_NODE_TYPES.TSPropertySignature]: ts.PropertySignature; + [AST_NODE_TYPES.TSQualifiedName]: ts.QualifiedName; + [AST_NODE_TYPES.TSRestType]: ts.RestTypeNode; + [AST_NODE_TYPES.TSThisType]: ts.ThisTypeNode; + [AST_NODE_TYPES.TSTupleType]: ts.TupleTypeNode; + [AST_NODE_TYPES.TSTypeAliasDeclaration]: ts.TypeAliasDeclaration; + [AST_NODE_TYPES.TSTypeAnnotation]: undefined; + [AST_NODE_TYPES.TSTypeAssertion]: ts.TypeAssertion; + [AST_NODE_TYPES.TSTypeLiteral]: ts.TypeLiteralNode; + [AST_NODE_TYPES.TSTypeOperator]: ts.TypeOperatorNode; + [AST_NODE_TYPES.TSTypeParameter]: ts.TypeParameterDeclaration; + [AST_NODE_TYPES.TSTypeParameterDeclaration]: undefined; + [AST_NODE_TYPES.TSTypeParameterInstantiation]: + | ts.TaggedTemplateExpression + | ts.ImportTypeNode + | ts.ExpressionWithTypeArguments + | ts.TypeReferenceNode + | ts.JsxOpeningElement + | ts.JsxSelfClosingElement + | ts.NewExpression + | ts.CallExpression; + [AST_NODE_TYPES.TSTypePredicate]: ts.TypePredicateNode; + [AST_NODE_TYPES.TSTypeQuery]: ts.TypeQueryNode; + [AST_NODE_TYPES.TSTypeReference]: ts.TypeReferenceNode; + [AST_NODE_TYPES.TSUnionType]: ts.UnionTypeNode; + [AST_NODE_TYPES.UpdateExpression]: + | ts.PrefixUnaryExpression + | ts.PostfixUnaryExpression; + [AST_NODE_TYPES.UnaryExpression]: + | ts.PrefixUnaryExpression + | ts.PostfixUnaryExpression + | ts.DeleteExpression + | ts.VoidExpression + | ts.TypeOfExpression; + [AST_NODE_TYPES.VariableDeclaration]: + | ts.VariableDeclarationList + | ts.VariableStatement; + [AST_NODE_TYPES.VariableDeclarator]: ts.VariableDeclaration; + [AST_NODE_TYPES.WhileStatement]: ts.WhileStatement; + [AST_NODE_TYPES.WithStatement]: ts.WithStatement; + [AST_NODE_TYPES.YieldExpression]: ts.YieldExpression; + + // Added by parser + // Should be same as AST_NODE_TYPES.FunctionExpression + [AST_NODE_TYPES.TSEmptyBodyFunctionExpression]: + | ts.FunctionExpression + | ts.ConstructorDeclaration + | ts.GetAccessorDeclaration + | ts.SetAccessorDeclaration + | ts.MethodDeclaration; + + // Keywords + [AST_NODE_TYPES.TSAbstractKeyword]: ts.Token; + [AST_NODE_TYPES.TSNullKeyword]: ts.NullLiteral | ts.KeywordTypeNode; + + [AST_NODE_TYPES.TSAnyKeyword]: ts.KeywordTypeNode; + [AST_NODE_TYPES.TSBigIntKeyword]: ts.KeywordTypeNode; + [AST_NODE_TYPES.TSBooleanKeyword]: ts.KeywordTypeNode; + [AST_NODE_TYPES.TSNeverKeyword]: ts.KeywordTypeNode; + [AST_NODE_TYPES.TSNumberKeyword]: ts.KeywordTypeNode; + [AST_NODE_TYPES.TSObjectKeyword]: ts.KeywordTypeNode; + [AST_NODE_TYPES.TSStringKeyword]: ts.KeywordTypeNode; + [AST_NODE_TYPES.TSSymbolKeyword]: ts.KeywordTypeNode; + [AST_NODE_TYPES.TSUnknownKeyword]: ts.KeywordTypeNode; + [AST_NODE_TYPES.TSVoidKeyword]: ts.KeywordTypeNode; + [AST_NODE_TYPES.TSUndefinedKeyword]: ts.KeywordTypeNode; + + // Unused + [AST_NODE_TYPES.TSAsyncKeyword]: ts.Token; + [AST_NODE_TYPES.TSDeclareKeyword]: ts.Token; + [AST_NODE_TYPES.TSExportKeyword]: ts.Token; + [AST_NODE_TYPES.TSStaticKeyword]: ts.Token; + [AST_NODE_TYPES.TSPublicKeyword]: ts.Token; + [AST_NODE_TYPES.TSPrivateKeyword]: ts.Token; + [AST_NODE_TYPES.TSProtectedKeyword]: ts.Token; + [AST_NODE_TYPES.TSReadonlyKeyword]: ts.Token; +} + +/** + * Maps TSESTree AST Node type to the expected TypeScript AST Node type(s). + * This mapping is based on the internal logic of the parser. + */ +export type TSESTreeToTSNode = Extract< + TSNode | ts.Token, + EstreeToTsNodeTypes[T['type']] +>; diff --git a/packages/typescript-estree/src/ts-estree/index.ts b/packages/typescript-estree/src/ts-estree/index.ts index 5bed681f209b..459edb578779 100644 --- a/packages/typescript-estree/src/ts-estree/index.ts +++ b/packages/typescript-estree/src/ts-estree/index.ts @@ -3,3 +3,4 @@ import * as TSESTree from './ts-estree'; export { TSESTree }; export * from './ast-node-types'; export * from './ts-nodes'; +export * from './estree-to-ts-node-types'; diff --git a/packages/typescript-estree/src/ts-estree/ts-nodes.ts b/packages/typescript-estree/src/ts-estree/ts-nodes.ts index eb99af207807..825aceab97d3 100644 --- a/packages/typescript-estree/src/ts-estree/ts-nodes.ts +++ b/packages/typescript-estree/src/ts-estree/ts-nodes.ts @@ -1,5 +1,7 @@ import * as ts from 'typescript'; +export type TSToken = ts.Token; + export type TSNode = ts.Node & ( | ts.Modifier @@ -30,7 +32,7 @@ export type TSNode = ts.Node & | ts.GetAccessorDeclaration | ts.SetAccessorDeclaration | ts.IndexSignatureDeclaration - | ts.KeywordTypeNode + | ts.KeywordTypeNode // TODO: This node is bad, maybe we should report this | ts.ImportTypeNode | ts.ThisTypeNode // | ts.FunctionOrConstructorTypeNodeBase -> FunctionTypeNode, ConstructorTypeNode diff --git a/packages/typescript-estree/tests/ast-alignment/fixtures-to-test.ts b/packages/typescript-estree/tests/ast-alignment/fixtures-to-test.ts index fec14747c464..ab677004678a 100644 --- a/packages/typescript-estree/tests/ast-alignment/fixtures-to-test.ts +++ b/packages/typescript-estree/tests/ast-alignment/fixtures-to-test.ts @@ -125,21 +125,18 @@ jsxFilesWithKnownIssues.push('invalid-no-tag-name'); tester.addFixturePatternConfig('javascript/basics'); -tester.addFixturePatternConfig('comments', { +tester.addFixturePatternConfig('comments'); + +tester.addFixturePatternConfig('javascript/templateStrings', { ignore: [ /** - * Template strings seem to also be affected by the difference in opinion between different parsers in: - * https://github.com/babel/babel/issues/6681 + * [BABEL ERRORED, BUT TS-ESTREE DID NOT] + * SyntaxError: Invalid escape sequence in template */ - 'no-comment-template', // Purely AST diffs - 'template-string-block', // Purely AST diffs + 'error-octal-literal', ], }); -tester.addFixturePatternConfig('javascript/templateStrings', { - ignore: ['**/*'], -}); - tester.addFixturePatternConfig('javascript/arrayLiteral'); tester.addFixturePatternConfig('javascript/simple-literals'); @@ -189,7 +186,19 @@ tester.addFixturePatternConfig('javascript/function', { ], }); -tester.addFixturePatternConfig('javascript/bigIntLiterals'); +tester.addFixturePatternConfig('javascript/bigIntLiterals', { + ignore: [ + /** + * new BigIntLiteral type + * @see https://github.com/estree/estree/blob/master/es2020.md#bigintliteral + * @see https://github.com/typescript-eslint/typescript-eslint/pull/1389 + */ + 'binary', + 'decimal', + 'hex', + 'octal', + ], +}); tester.addFixturePatternConfig('javascript/binaryLiterals'); tester.addFixturePatternConfig('javascript/blockBindings'); @@ -220,7 +229,16 @@ tester.addFixturePatternConfig('javascript/destructuring-and-forOf'); tester.addFixturePatternConfig('javascript/destructuring-and-spread'); tester.addFixturePatternConfig('javascript/experimentalAsyncIteration'); -tester.addFixturePatternConfig('javascript/experimentalDynamicImport'); +tester.addFixturePatternConfig('javascript/experimentalDynamicImport', { + ignore: [ + /** + * new ImportExpression type + * @see https://github.com/estree/estree/blob/master/es2020.md#importexpression + * @see https://github.com/typescript-eslint/typescript-eslint/pull/1389 + */ + 'dynamic-import', + ], +}); tester.addFixturePatternConfig('javascript/exponentiationOperators'); tester.addFixturePatternConfig('javascript/experimentalOptionalCatchBinding'); @@ -442,19 +460,13 @@ tester.addFixturePatternConfig('typescript/decorators/property-decorators', { tester.addFixturePatternConfig('typescript/expressions', { fileType: 'ts', - ignore: [ - /** - * there is difference in range between babel and ts-estree - */ - 'tagged-template-expression-type-arguments', - ], }); tester.addFixturePatternConfig('typescript/errorRecovery', { fileType: 'ts', ignore: [ /** - * [TS-ESTREE ERRORED, BUT BABEL DID NOT] + * [BABEL ERRORED, BUT TS-ESTREE DID NOT] * TODO: enable error code TS1019: An index signature parameter cannot have a question mark. */ 'interface-with-optional-index-signature', diff --git a/packages/typescript-estree/tests/ast-alignment/utils.ts b/packages/typescript-estree/tests/ast-alignment/utils.ts index 4830a7f1f5a5..1ad1ba3bba17 100644 --- a/packages/typescript-estree/tests/ast-alignment/utils.ts +++ b/packages/typescript-estree/tests/ast-alignment/utils.ts @@ -130,6 +130,7 @@ export function preprocessBabylonAST(ast: BabelTypes.File): any { /** * We want this node to be different * @see https://github.com/JamesHenry/typescript-estree/issues/109 + * @see https://github.com/prettier/prettier/pull/5728 */ TSTypeParameter(node: any) { if (node.name) { @@ -178,7 +179,9 @@ export function preprocessBabylonAST(ast: BabelTypes.File): any { node.type = 'TSClassImplements'; } }, - // https://github.com/prettier/prettier/issues/5817 + /** + * @see https://github.com/prettier/prettier/issues/5817 + */ FunctionExpression(node: any, parent: any) { if (parent.typeParameters && parent.type === 'Property') { node.typeParameters = parent.typeParameters; @@ -196,6 +199,25 @@ export function preprocessBabylonAST(ast: BabelTypes.File): any { node.loc.start = Object.assign({}, node.typeParameters.loc.start); } }, + /** + * Template strings seem to also be affected by the difference in opinion between different parsers in + * @see https://github.com/babel/babel/issues/6681 + * @see https://github.com/babel/babel-eslint/blob/master/lib/babylon-to-espree/convertAST.js#L81-L96 + */ + TemplateLiteral(node: any) { + for (let j = 0; j < node.quasis.length; j++) { + const q = node.quasis[j]; + q.range[0] -= 1; + q.loc.start.column -= 1; + if (q.tail) { + q.range[1] += 1; + q.loc.end.column += 1; + } else { + q.range[1] += 2; + q.loc.end.column += 2; + } + } + }, /** * TS 3.7: optional chaining * babel: sets optional property as true/undefined diff --git a/packages/typescript-estree/tests/lib/__snapshots__/parse.ts.snap b/packages/typescript-estree/tests/lib/__snapshots__/parse.ts.snap index 616c95d748e2..4b6d7f2c7839 100644 --- a/packages/typescript-estree/tests/lib/__snapshots__/parse.ts.snap +++ b/packages/typescript-estree/tests/lib/__snapshots__/parse.ts.snap @@ -313,57 +313,57 @@ Object { exports[`parse() invalid file error messages "parserOptions.extraFileExtensions" is empty the extension does not match 1`] = ` "\\"parserOptions.project\\" has been set for @typescript-eslint/parser. -The file does not match your project config: tests/fixtures/invalidFileErrors/other/unknownFileType.unknown. +The file does not match your project config: other/unknownFileType.unknown. The extension for the file (.unknown) is non-standard. You should add \\"parserOptions.extraFileExtensions\\" to your config." `; exports[`parse() invalid file error messages "parserOptions.extraFileExtensions" is non-empty invalid extension 1`] = ` "\\"parserOptions.project\\" has been set for @typescript-eslint/parser. -The file does not match your project config: tests/fixtures/invalidFileErrors/other/unknownFileType.unknown. +The file does not match your project config: other/unknownFileType.unknown. Found unexpected extension \\"unknown\\" specified with the \\"extraFileExtensions\\" option. Did you mean \\".unknown\\"? The extension for the file (.unknown) is non-standard. It should be added to your existing \\"parserOptions.extraFileExtensions\\"." `; exports[`parse() invalid file error messages "parserOptions.extraFileExtensions" is non-empty the extension does not match 1`] = ` "\\"parserOptions.project\\" has been set for @typescript-eslint/parser. -The file does not match your project config: tests/fixtures/invalidFileErrors/other/unknownFileType.unknown. +The file does not match your project config: other/unknownFileType.unknown. The extension for the file (.unknown) is non-standard. It should be added to your existing \\"parserOptions.extraFileExtensions\\"." `; exports[`parse() invalid file error messages "parserOptions.extraFileExtensions" is non-empty the extension matches duplicate extension 1`] = ` "\\"parserOptions.project\\" has been set for @typescript-eslint/parser. -The file does not match your project config: tests/fixtures/invalidFileErrors/ts/notIncluded.ts. +The file does not match your project config: ts/notIncluded.ts. You unnecessarily included the extension \\".ts\\" with the \\"extraFileExtensions\\" option. This extension is already handled by the parser by default. The file must be included in at least one of the projects provided." `; exports[`parse() invalid file error messages "parserOptions.extraFileExtensions" is non-empty the extension matches the file isn't included 1`] = ` "\\"parserOptions.project\\" has been set for @typescript-eslint/parser. -The file does not match your project config: tests/fixtures/invalidFileErrors/other/notIncluded.vue. +The file does not match your project config: other/notIncluded.vue. The file must be included in at least one of the projects provided." `; exports[`parse() invalid file error messages project includes errors for not included files 1`] = ` "\\"parserOptions.project\\" has been set for @typescript-eslint/parser. -The file does not match your project config: tests/fixtures/invalidFileErrors/ts/notIncluded.ts. +The file does not match your project config: ts/notIncluded.ts. The file must be included in at least one of the projects provided." `; exports[`parse() invalid file error messages project includes errors for not included files 2`] = ` "\\"parserOptions.project\\" has been set for @typescript-eslint/parser. -The file does not match your project config: tests/fixtures/invalidFileErrors/ts/notIncluded.tsx. +The file does not match your project config: ts/notIncluded.tsx. The file must be included in at least one of the projects provided." `; exports[`parse() invalid file error messages project includes errors for not included files 3`] = ` "\\"parserOptions.project\\" has been set for @typescript-eslint/parser. -The file does not match your project config: tests/fixtures/invalidFileErrors/js/notIncluded.js. +The file does not match your project config: js/notIncluded.js. The file must be included in at least one of the projects provided." `; exports[`parse() invalid file error messages project includes errors for not included files 4`] = ` "\\"parserOptions.project\\" has been set for @typescript-eslint/parser. -The file does not match your project config: tests/fixtures/invalidFileErrors/js/notIncluded.jsx. +The file does not match your project config: js/notIncluded.jsx. The file must be included in at least one of the projects provided." `; diff --git a/packages/typescript-estree/tests/lib/parse.ts b/packages/typescript-estree/tests/lib/parse.ts index 6cba9ba867b0..1e6a1e972360 100644 --- a/packages/typescript-estree/tests/lib/parse.ts +++ b/packages/typescript-estree/tests/lib/parse.ts @@ -1,7 +1,9 @@ +import debug from 'debug'; import { join, resolve } from 'path'; import * as parser from '../../src/parser'; import * as astConverter from '../../src/ast-converter'; import { TSESTreeOptions } from '../../src/parser-options'; +import * as sharedParserUtils from '../../src/create-program/shared'; import { createSnapshotTestBlock } from '../../tools/test-utils'; const FIXTURES_DIR = './tests/fixtures/simpleProject'; @@ -144,7 +146,7 @@ describe('parse()', () => { tokens: true, range: true, loc: true, - filePath: 'tests/fixtures/simpleProject/file.ts', + filePath: 'file.ts', }; const projectConfig: TSESTreeOptions = { ...baseConfig, @@ -496,4 +498,63 @@ describe('parse()', () => { }); }); }); + + describe('debug options', () => { + const debugEnable = jest.fn(); + beforeEach(() => { + debugEnable.mockReset(); + debug.enable = debugEnable; + jest.spyOn(debug, 'enabled').mockImplementation(() => false); + }); + + it("shouldn't turn on debugger if no options were provided", () => { + parser.parseAndGenerateServices('const x = 1;', { + debugLevel: [], + }); + expect(debugEnable).not.toHaveBeenCalled(); + }); + + it('should turn on eslint debugger', () => { + parser.parseAndGenerateServices('const x = 1;', { + debugLevel: ['eslint'], + }); + expect(debugEnable).toHaveBeenCalledTimes(1); + expect(debugEnable).toHaveBeenCalledWith('eslint:*,-eslint:code-path'); + }); + + it('should turn on typescript-eslint debugger', () => { + parser.parseAndGenerateServices('const x = 1;', { + debugLevel: ['typescript-eslint'], + }); + expect(debugEnable).toHaveBeenCalledTimes(1); + expect(debugEnable).toHaveBeenCalledWith('typescript-eslint:*'); + }); + + it('should turn on both eslint and typescript-eslint debugger', () => { + parser.parseAndGenerateServices('const x = 1;', { + debugLevel: ['typescript-eslint', 'eslint'], + }); + expect(debugEnable).toHaveBeenCalledTimes(1); + expect(debugEnable).toHaveBeenCalledWith( + 'typescript-eslint:*,eslint:*,-eslint:code-path', + ); + }); + + it('should turn on typescript debugger', () => { + const spy = jest.spyOn( + sharedParserUtils, + 'createDefaultCompilerOptionsFromExtra', + ); + + parser.parseAndGenerateServices('const x = 1;', { + debugLevel: ['typescript'], + }); + expect(spy).toHaveBeenCalled(); + expect(spy).toHaveReturnedWith( + expect.objectContaining({ + extendedDiagnostics: true, + }), + ); + }); + }); }); diff --git a/packages/typescript-estree/tests/lib/persistentParse.ts b/packages/typescript-estree/tests/lib/persistentParse.ts index 8d52f3825878..98f5b1bcd0db 100644 --- a/packages/typescript-estree/tests/lib/persistentParse.ts +++ b/packages/typescript-estree/tests/lib/persistentParse.ts @@ -7,8 +7,10 @@ const CONTENTS = { foo: 'console.log("foo")', bar: 'console.log("bar")', 'baz/bar': 'console.log("baz bar")', + 'bat/baz/bar': 'console.log("bat/baz/bar")', }; +const cwdCopy = process.cwd(); const tmpDirs = new Set(); afterEach(() => { // stop watching the files and folders @@ -17,12 +19,15 @@ afterEach(() => { // clean up the temporary files and folders tmpDirs.forEach(t => t.removeCallback()); tmpDirs.clear(); + + // restore original cwd + process.chdir(cwdCopy); }); function writeTSConfig(dirName: string, config: Record): void { fs.writeFileSync(path.join(dirName, 'tsconfig.json'), JSON.stringify(config)); } -function writeFile(dirName: string, file: 'foo' | 'bar' | 'baz/bar'): void { +function writeFile(dirName: string, file: keyof typeof CONTENTS): void { fs.writeFileSync(path.join(dirName, 'src', `${file}.ts`), CONTENTS[file]); } function renameFile(dirName: string, src: 'bar', dest: 'baz/bar'): void { @@ -53,14 +58,25 @@ function setup(tsconfig: Record, writeBar = true): string { return tmpDir.name; } -function parseFile(filename: 'foo' | 'bar' | 'baz/bar', tmpDir: string): void { - parseAndGenerateServices(CONTENTS.foo, { +function parseFile( + filename: keyof typeof CONTENTS, + tmpDir: string, + relative?: boolean, + ignoreTsconfigRootDir?: boolean, +): void { + parseAndGenerateServices(CONTENTS[filename], { project: './tsconfig.json', - tsconfigRootDir: tmpDir, - filePath: path.join(tmpDir, 'src', `${filename}.ts`), + tsconfigRootDir: ignoreTsconfigRootDir ? undefined : tmpDir, + filePath: relative + ? path.join('src', `${filename}.ts`) + : path.join(tmpDir, 'src', `${filename}.ts`), }); } +function existsSync(filename: keyof typeof CONTENTS, tmpDir = ''): boolean { + return fs.existsSync(path.join(tmpDir, 'src', `${filename}.ts`)); +} + function baseTests( tsConfigExcludeBar: Record, tsConfigIncludeAll: Record, @@ -97,34 +113,54 @@ function baseTests( it('allows parsing of deeply nested new files', () => { const PROJECT_DIR = setup(tsConfigIncludeAll, false); + const bazSlashBar = path.join('baz', 'bar') as 'baz/bar'; // parse once to: assert the config as correct, and to make sure the program is setup expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow(); // bar should throw because it doesn't exist yet - expect(() => parseFile('baz/bar', PROJECT_DIR)).toThrow(); + expect(() => parseFile(bazSlashBar, PROJECT_DIR)).toThrow(); // write a new file and attempt to parse it - writeFile(PROJECT_DIR, 'baz/bar'); + writeFile(PROJECT_DIR, bazSlashBar); // both files should parse fine now expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow(); - expect(() => parseFile('baz/bar', PROJECT_DIR)).not.toThrow(); + expect(() => parseFile(bazSlashBar, PROJECT_DIR)).not.toThrow(); + }); + + it('allows parsing of deeply nested new files in new folder', () => { + const PROJECT_DIR = setup(tsConfigIncludeAll); + + expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow(); + + // Create deep folder structure after first parse (this is important step) + // context: https://github.com/typescript-eslint/typescript-eslint/issues/1394 + fs.mkdirSync(path.join(PROJECT_DIR, 'src', 'bat')); + fs.mkdirSync(path.join(PROJECT_DIR, 'src', 'bat', 'baz')); + + const bazSlashBar = path.join('bat', 'baz', 'bar') as 'bat/baz/bar'; + + // write a new file and attempt to parse it + writeFile(PROJECT_DIR, bazSlashBar); + + expect(() => parseFile(bazSlashBar, PROJECT_DIR)).not.toThrow(); }); it('allows renaming of files', () => { const PROJECT_DIR = setup(tsConfigIncludeAll, true); + const bazSlashBar = path.join('baz', 'bar') as 'baz/bar'; // parse once to: assert the config as correct, and to make sure the program is setup expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow(); // bar should throw because it doesn't exist yet - expect(() => parseFile('baz/bar', PROJECT_DIR)).toThrow(); + expect(() => parseFile(bazSlashBar, PROJECT_DIR)).toThrow(); // write a new file and attempt to parse it - renameFile(PROJECT_DIR, 'bar', 'baz/bar'); + renameFile(PROJECT_DIR, 'bar', bazSlashBar); // both files should parse fine now expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow(); - expect(() => parseFile('baz/bar', PROJECT_DIR)).not.toThrow(); + expect(() => parseFile(bazSlashBar, PROJECT_DIR)).not.toThrow(); }); it('reacts to changes in the tsconfig', () => { @@ -141,35 +177,45 @@ function baseTests( expect(() => parseFile('bar', PROJECT_DIR)).not.toThrow(); }); - it('handles tsconfigs with no includes/excludes (single level)', () => { - const PROJECT_DIR = setup({}, false); + it('should work with relative paths', () => { + const PROJECT_DIR = setup(tsConfigIncludeAll, false); // parse once to: assert the config as correct, and to make sure the program is setup - expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow(); - expect(() => parseFile('bar', PROJECT_DIR)).toThrow(); + expect(() => parseFile('foo', PROJECT_DIR, true)).not.toThrow(); + // bar should throw because it doesn't exist yet + expect(() => parseFile('bar', PROJECT_DIR, true)).toThrow(); // write a new file and attempt to parse it writeFile(PROJECT_DIR, 'bar'); - expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow(); - expect(() => parseFile('bar', PROJECT_DIR)).not.toThrow(); + // make sure that file is correctly created + expect(existsSync('bar', PROJECT_DIR)).toEqual(true); + + // both files should parse fine now + expect(() => parseFile('foo', PROJECT_DIR, true)).not.toThrow(); + expect(() => parseFile('bar', PROJECT_DIR, true)).not.toThrow(); }); - it('handles tsconfigs with no includes/excludes (nested)', () => { - const PROJECT_DIR = setup({}, false); + it('should work with relative paths without tsconfig root', () => { + const PROJECT_DIR = setup(tsConfigIncludeAll, false); + process.chdir(PROJECT_DIR); // parse once to: assert the config as correct, and to make sure the program is setup - expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow(); - expect(() => parseFile('baz/bar', PROJECT_DIR)).toThrow(); + expect(() => parseFile('foo', PROJECT_DIR, true, true)).not.toThrow(); + // bar should throw because it doesn't exist yet + expect(() => parseFile('bar', PROJECT_DIR, true, true)).toThrow(); // write a new file and attempt to parse it - writeFile(PROJECT_DIR, 'baz/bar'); + writeFile(PROJECT_DIR, 'bar'); - expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow(); - expect(() => parseFile('baz/bar', PROJECT_DIR)).not.toThrow(); - }); + // make sure that file is correctly created + expect(existsSync('bar')).toEqual(true); + expect(existsSync('bar', PROJECT_DIR)).toEqual(true); - // TODO - support the complex monorepo case with a tsconfig with no include/exclude + // both files should parse fine now + expect(() => parseFile('foo', PROJECT_DIR, true, true)).not.toThrow(); + expect(() => parseFile('bar', PROJECT_DIR, true, true)).not.toThrow(); + }); } describe('persistent parse', () => { @@ -213,5 +259,49 @@ describe('persistent parse', () => { const tsConfigIncludeAll = {}; baseTests(tsConfigExcludeBar, tsConfigIncludeAll); + + it('handles tsconfigs with no includes/excludes (single level)', () => { + const PROJECT_DIR = setup({}, false); + + // parse once to: assert the config as correct, and to make sure the program is setup + expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow(); + expect(() => parseFile('bar', PROJECT_DIR)).toThrow(); + + // write a new file and attempt to parse it + writeFile(PROJECT_DIR, 'bar'); + + expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow(); + expect(() => parseFile('bar', PROJECT_DIR)).not.toThrow(); + }); + + it('handles tsconfigs with no includes/excludes (nested)', () => { + const PROJECT_DIR = setup({}, false); + const bazSlashBar = path.join('baz', 'bar') as 'baz/bar'; + + // parse once to: assert the config as correct, and to make sure the program is setup + expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow(); + expect(() => parseFile(bazSlashBar, PROJECT_DIR)).toThrow(); + + // write a new file and attempt to parse it + writeFile(PROJECT_DIR, bazSlashBar); + + expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow(); + expect(() => parseFile(bazSlashBar, PROJECT_DIR)).not.toThrow(); + }); + }); + + /* + If there is no includes, then typescript will ask for a slightly different set of watchers. + */ + describe('tsconfig with overlapping globs', () => { + const tsConfigExcludeBar = { + include: ['./*', './**/*', './src/**/*'], + exclude: ['./src/bar.ts'], + }; + const tsConfigIncludeAll = { + include: ['./*', './**/*', './src/**/*'], + }; + + baseTests(tsConfigExcludeBar, tsConfigIncludeAll); }); }); diff --git a/packages/typescript-estree/tests/lib/semanticInfo.ts b/packages/typescript-estree/tests/lib/semanticInfo.ts index 3a99890c401f..5f979fb94add 100644 --- a/packages/typescript-estree/tests/lib/semanticInfo.ts +++ b/packages/typescript-estree/tests/lib/semanticInfo.ts @@ -16,7 +16,9 @@ import { import { TSESTree } from '../../src/ts-estree'; const FIXTURES_DIR = './tests/fixtures/semanticInfo'; -const testFiles = glob.sync(`${FIXTURES_DIR}/**/*.src.ts`); +const testFiles = glob.sync(`**/*.src.ts`, { + cwd: FIXTURES_DIR, +}); function createOptions(fileName: string): TSESTreeOptions & { cwd?: string } { return { @@ -40,7 +42,7 @@ beforeEach(() => clearCaches()); describe('semanticInfo', () => { // test all AST snapshots testFiles.forEach(filename => { - const code = readFileSync(filename, 'utf8'); + const code = readFileSync(join(FIXTURES_DIR, filename), 'utf8'); it( formatSnapshotName(filename, FIXTURES_DIR, extname(filename)), createSnapshotTestBlock( @@ -53,7 +55,7 @@ describe('semanticInfo', () => { it(`should cache the created ts.program`, () => { const filename = testFiles[0]; - const code = readFileSync(filename, 'utf8'); + const code = readFileSync(join(FIXTURES_DIR, filename), 'utf8'); const options = createOptions(filename); const optionsProjectString = { ...options, @@ -68,7 +70,7 @@ describe('semanticInfo', () => { it(`should handle "project": "./tsconfig.json" and "project": ["./tsconfig.json"] the same`, () => { const filename = testFiles[0]; - const code = readFileSync(filename, 'utf8'); + const code = readFileSync(join(FIXTURES_DIR, filename), 'utf8'); const options = createOptions(filename); const optionsProjectString = { ...options, @@ -85,7 +87,7 @@ describe('semanticInfo', () => { it(`should resolve absolute and relative tsconfig paths the same`, () => { const filename = testFiles[0]; - const code = readFileSync(filename, 'utf8'); + const code = readFileSync(join(FIXTURES_DIR, filename), 'utf8'); const options = createOptions(filename); const optionsAbsolutePath = { ...options, diff --git a/yarn.lock b/yarn.lock index 56703239ab25..a80b58ca2c51 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10,18 +10,18 @@ "@babel/highlight" "^7.0.0" "@babel/core@^7.1.0": - version "7.6.2" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.6.2.tgz#069a776e8d5e9eefff76236bc8845566bd31dd91" - integrity sha512-l8zto/fuoZIbncm+01p8zPSDZu/VuuJhAfA7d/AbzM09WR7iVhavvfNDYCNpo1VvLk6E6xgAoP9P+/EMJHuRkQ== + version "7.7.7" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.7.7.tgz#ee155d2e12300bcc0cff6a8ad46f2af5063803e9" + integrity sha512-jlSjuj/7z138NLZALxVgrx13AOtqip42ATZP7+kYl53GvDV6+4dCek1mVUo8z8c8Xnw/mx2q3d9HWh3griuesQ== dependencies: "@babel/code-frame" "^7.5.5" - "@babel/generator" "^7.6.2" - "@babel/helpers" "^7.6.2" - "@babel/parser" "^7.6.2" - "@babel/template" "^7.6.0" - "@babel/traverse" "^7.6.2" - "@babel/types" "^7.6.0" - convert-source-map "^1.1.0" + "@babel/generator" "^7.7.7" + "@babel/helpers" "^7.7.4" + "@babel/parser" "^7.7.7" + "@babel/template" "^7.7.4" + "@babel/traverse" "^7.7.4" + "@babel/types" "^7.7.4" + convert-source-map "^1.7.0" debug "^4.1.0" json5 "^2.1.0" lodash "^4.17.13" @@ -29,52 +29,52 @@ semver "^5.4.1" source-map "^0.5.0" -"@babel/generator@^7.4.0", "@babel/generator@^7.6.2": - version "7.6.2" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.6.2.tgz#dac8a3c2df118334c2a29ff3446da1636a8f8c03" - integrity sha512-j8iHaIW4gGPnViaIHI7e9t/Hl8qLjERI6DcV9kEpAIDJsAOrcnXqRS7t+QbhL76pwbtqP+QCQLL0z1CyVmtjjQ== +"@babel/generator@^7.4.0", "@babel/generator@^7.7.4", "@babel/generator@^7.7.7": + version "7.7.7" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.7.7.tgz#859ac733c44c74148e1a72980a64ec84b85f4f45" + integrity sha512-/AOIBpHh/JU1l0ZFS4kiRCBnLi6OTHzh0RPk3h9isBxkkqELtQNFi1Vr/tiG9p1yfoUdKVwISuXWQR+hwwM4VQ== dependencies: - "@babel/types" "^7.6.0" + "@babel/types" "^7.7.4" jsesc "^2.5.1" lodash "^4.17.13" source-map "^0.5.0" -"@babel/helper-function-name@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz#a0ceb01685f73355d4360c1247f582bfafc8ff53" - integrity sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw== +"@babel/helper-function-name@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.7.4.tgz#ab6e041e7135d436d8f0a3eca15de5b67a341a2e" + integrity sha512-AnkGIdiBhEuiwdoMnKm7jfPfqItZhgRaZfMg1XX3bS25INOnLPjPG1Ppnajh8eqgt5kPJnfqrRHqFqmjKDZLzQ== dependencies: - "@babel/helper-get-function-arity" "^7.0.0" - "@babel/template" "^7.1.0" - "@babel/types" "^7.0.0" + "@babel/helper-get-function-arity" "^7.7.4" + "@babel/template" "^7.7.4" + "@babel/types" "^7.7.4" -"@babel/helper-get-function-arity@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz#83572d4320e2a4657263734113c42868b64e49c3" - integrity sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ== +"@babel/helper-get-function-arity@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.7.4.tgz#cb46348d2f8808e632f0ab048172130e636005f0" + integrity sha512-QTGKEdCkjgzgfJ3bAyRwF4yyT3pg+vDgan8DSivq1eS0gwi+KGKE5x8kRcbeFTb/673mkO5SN1IZfmCfA5o+EA== dependencies: - "@babel/types" "^7.0.0" + "@babel/types" "^7.7.4" "@babel/helper-plugin-utils@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0.tgz#bbb3fbee98661c569034237cc03967ba99b4f250" integrity sha512-CYAOUCARwExnEixLdB6sDm2dIJ/YgEAKDM1MOeMeZu9Ld/bDgVo8aiWrXwcY7OBh+1Ea2uUcVRcxKk0GJvW7QA== -"@babel/helper-split-export-declaration@^7.4.4": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz#ff94894a340be78f53f06af038b205c49d993677" - integrity sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q== +"@babel/helper-split-export-declaration@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.7.4.tgz#57292af60443c4a3622cf74040ddc28e68336fd8" + integrity sha512-guAg1SXFcVr04Guk9eq0S4/rWS++sbmyqosJzVs8+1fH5NI+ZcmkaSkc7dmtAFbHFva6yRJnjW3yAcGxjueDug== dependencies: - "@babel/types" "^7.4.4" + "@babel/types" "^7.7.4" -"@babel/helpers@^7.6.2": - version "7.6.2" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.6.2.tgz#681ffe489ea4dcc55f23ce469e58e59c1c045153" - integrity sha512-3/bAUL8zZxYs1cdX2ilEE0WobqbCmKWr/889lf2SS0PpDcpEIY8pb1CCyz0pEcX3pEb+MCbks1jIokz2xLtGTA== +"@babel/helpers@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.7.4.tgz#62c215b9e6c712dadc15a9a0dcab76c92a940302" + integrity sha512-ak5NGZGJ6LV85Q1Zc9gn2n+ayXOizryhjSUBTdu5ih1tlVCJeuQENzc4ItyCVhINVXvIT/ZQ4mheGIsfBkpskg== dependencies: - "@babel/template" "^7.6.0" - "@babel/traverse" "^7.6.2" - "@babel/types" "^7.6.0" + "@babel/template" "^7.7.4" + "@babel/traverse" "^7.7.4" + "@babel/types" "^7.7.4" "@babel/highlight@^7.0.0": version "7.5.0" @@ -85,20 +85,15 @@ esutils "^2.0.2" js-tokens "^4.0.0" -"@babel/parser@7.7.5": - version "7.7.5" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.7.5.tgz#cbf45321619ac12d83363fcf9c94bb67fa646d71" - integrity sha512-KNlOe9+/nk4i29g0VXgl8PEXIRms5xKLJeuZ6UptN0fHv+jDiriG+y94X6qAgWTR0h3KaoM1wK5G5h7MHFRSig== - -"@babel/parser@^7.1.0", "@babel/parser@^7.4.3", "@babel/parser@^7.6.0", "@babel/parser@^7.6.2": - version "7.6.2" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.6.2.tgz#205e9c95e16ba3b8b96090677a67c9d6075b70a1" - integrity sha512-mdFqWrSPCmikBoaBYMuBulzTIKuXVPtEISFbRRVNwMWpCms/hmE2kRq0bblUHaNRKrjRlmVbx1sDHmjmRgD2Xg== +"@babel/parser@7.7.7", "@babel/parser@^7.1.0", "@babel/parser@^7.4.3", "@babel/parser@^7.7.4", "@babel/parser@^7.7.7": + version "7.7.7" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.7.7.tgz#1b886595419cf92d811316d5b715a53ff38b4937" + integrity sha512-WtTZMZAZLbeymhkd/sEaPD8IQyGAhmuTuvTzLiCFM7iXiVdY0gc0IaI+cW0fh1BnSMbJSzXX6/fHllgHKwHhXw== "@babel/plugin-syntax-object-rest-spread@^7.0.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.2.0.tgz#3b7a3e733510c57e820b9142a6579ac8b0dfad2e" - integrity sha512-t0JKGgqk2We+9may3t0xDdmneaXmyxq0xieYcKHxIsrJO64n1OiMWNUtc5gQK1PA0NpdCRrtZp4z+IUaKugrSA== + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.7.4.tgz#47cf220d19d6d0d7b154304701f468fc1cc6ff46" + integrity sha512-mObR+r+KZq0XhRVS2BrBKBpr5jqrqzlPvS9C9vuOf5ilSwzloAl7RPWLrgKdWS6IreaVrjHxTjtyqFiOisaCwg== dependencies: "@babel/helper-plugin-utils" "^7.0.0" @@ -109,40 +104,31 @@ dependencies: regenerator-runtime "^0.13.2" -"@babel/template@^7.1.0", "@babel/template@^7.4.0", "@babel/template@^7.6.0": - version "7.6.0" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.6.0.tgz#7f0159c7f5012230dad64cca42ec9bdb5c9536e6" - integrity sha512-5AEH2EXD8euCk446b7edmgFdub/qfH1SN6Nii3+fyXP807QRx9Q73A2N5hNwRRslC2H9sNzaFhsPubkS4L8oNQ== +"@babel/template@^7.4.0", "@babel/template@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.7.4.tgz#428a7d9eecffe27deac0a98e23bf8e3675d2a77b" + integrity sha512-qUzihgVPguAzXCK7WXw8pqs6cEwi54s3E+HrejlkuWO6ivMKx9hZl3Y2fSXp9i5HgyWmj7RKP+ulaYnKM4yYxw== dependencies: "@babel/code-frame" "^7.0.0" - "@babel/parser" "^7.6.0" - "@babel/types" "^7.6.0" + "@babel/parser" "^7.7.4" + "@babel/types" "^7.7.4" -"@babel/traverse@^7.1.0", "@babel/traverse@^7.4.3", "@babel/traverse@^7.6.2": - version "7.6.2" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.6.2.tgz#b0e2bfd401d339ce0e6c05690206d1e11502ce2c" - integrity sha512-8fRE76xNwNttVEF2TwxJDGBLWthUkHWSldmfuBzVRmEDWOtu4XdINTgN7TDWzuLg4bbeIMLvfMFD9we5YcWkRQ== +"@babel/traverse@^7.1.0", "@babel/traverse@^7.4.3", "@babel/traverse@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.7.4.tgz#9c1e7c60fb679fe4fcfaa42500833333c2058558" + integrity sha512-P1L58hQyupn8+ezVA2z5KBm4/Zr4lCC8dwKCMYzsa5jFMDMQAzaBNy9W5VjB+KAmBjb40U7a/H6ao+Xo+9saIw== dependencies: "@babel/code-frame" "^7.5.5" - "@babel/generator" "^7.6.2" - "@babel/helper-function-name" "^7.1.0" - "@babel/helper-split-export-declaration" "^7.4.4" - "@babel/parser" "^7.6.2" - "@babel/types" "^7.6.0" + "@babel/generator" "^7.7.4" + "@babel/helper-function-name" "^7.7.4" + "@babel/helper-split-export-declaration" "^7.7.4" + "@babel/parser" "^7.7.4" + "@babel/types" "^7.7.4" debug "^4.1.0" globals "^11.1.0" lodash "^4.17.13" -"@babel/types@^7.0.0", "@babel/types@^7.3.0", "@babel/types@^7.4.0", "@babel/types@^7.4.4", "@babel/types@^7.6.0": - version "7.6.1" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.6.1.tgz#53abf3308add3ac2a2884d539151c57c4b3ac648" - integrity sha512-X7gdiuaCmA0uRjCmRtYJNAVCc/q+5xSgsfKJHqMN4iNLILX39677fJE1O40arPMh0TTtS9ItH67yre6c7k6t0g== - dependencies: - esutils "^2.0.2" - lodash "^4.17.13" - to-fast-properties "^2.0.0" - -"@babel/types@^7.7.4": +"@babel/types@^7.0.0", "@babel/types@^7.3.0", "@babel/types@^7.4.0", "@babel/types@^7.7.4": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.7.4.tgz#516570d539e44ddf308c07569c258ff94fde9193" integrity sha512-cz5Ji23KCi4T+YIE/BolWosrJuSmoZeN1EFnRtBwF+KKLi8GG/Z2c2hOJJeCXPk4mwk4QFvTmwIodJowXgttRA== @@ -1303,9 +1289,9 @@ "@types/babel__traverse" "*" "@types/babel__generator@*": - version "7.6.0" - resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.0.tgz#f1ec1c104d1bb463556ecb724018ab788d0c172a" - integrity sha512-c1mZUu4up5cp9KROs/QAw0gTeHrw/x7m52LcnvMxxOZ03DmLwPV0MlGmlgzV3cnSdjhJOZsj7E7FHeioai+egw== + version "7.6.1" + resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.1.tgz#4901767b397e8711aeb99df8d396d7ba7b7f0e04" + integrity sha512-bBKm+2VPJcMRVwNhxKu8W+5/zT7pwNEqeokFOmbvVSqGzFneNxYcEBro9Ac7/N9tlsaPYnZLK8J1LWKkMsLAew== dependencies: "@babel/types" "^7.0.0" @@ -1318,9 +1304,9 @@ "@babel/types" "^7.0.0" "@types/babel__traverse@*", "@types/babel__traverse@^7.0.6": - version "7.0.7" - resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.0.7.tgz#2496e9ff56196cc1429c72034e07eab6121b6f3f" - integrity sha512-CeBpmX1J8kWLcDEnI3Cl2Eo6RfbGvzUctA+CjZUhOKDFbLfcr7fc4usEqLNWetrlJd7RhAkyYe2czXop4fICpw== + version "7.0.8" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.0.8.tgz#479a4ee3e291a403a1096106013ec22cf9b64012" + integrity sha512-yGeB2dHEdvxjP0y4UbRtQaSkXJ9649fYCmIdRoul5kfAoGCwxuCbMhag0k3RPfnuh9kPGm8x89btcfDEXdVWGw== dependencies: "@babel/types" "^7.3.0" @@ -1390,24 +1376,10 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.3.tgz#bdfd69d61e464dcc81b25159c270d75a73c1a636" integrity sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A== -"@types/lodash.memoize@^4.1.4": - version "4.1.6" - resolved "https://registry.yarnpkg.com/@types/lodash.memoize/-/lodash.memoize-4.1.6.tgz#3221f981790a415cab1a239f25c17efd8b604c23" - integrity sha512-mYxjKiKzRadRJVClLKxS4wb3Iy9kzwJ1CkbyKiadVxejnswnRByyofmPMscFKscmYpl36BEEhCMPuWhA1R/1ZQ== - dependencies: - "@types/lodash" "*" - -"@types/lodash.unescape@^4.0.4": - version "4.0.6" - resolved "https://registry.yarnpkg.com/@types/lodash.unescape/-/lodash.unescape-4.0.6.tgz#cca470b1ab7736a81ea91bd071b2ed4f21ed4520" - integrity sha512-9JwlwdieWYlLqr1s/X8M1/Heo9qaFb2lHj6ETFlwJQrU9QyMvKngMnYzz0OYE4qx+VFdZgVP9I7AnBdju8/x0g== - dependencies: - "@types/lodash" "*" - -"@types/lodash@*": - version "4.14.141" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.141.tgz#d81f4d0c562abe28713406b571ffb27692a82ae6" - integrity sha512-v5NYIi9qEbFEUpCyikmnOYe4YlP8BMUdTcNCAquAKzu+FA7rZ1onj9x80mbnDdOW/K5bFf3Tv5kJplP33+gAbQ== +"@types/lodash@^4.14.149": + version "4.14.149" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.149.tgz#1342d63d948c6062838fbf961012f74d4e638440" + integrity sha512-ijGqzZt/b7BfzcK9vTrS6MFljQRPn5BFWOx8oE0GYxribu6uV+aA9zZuXI1zc/etK9E8nrgdoF2+LgUw7+9tJQ== "@types/marked@^0.7.1": version "0.7.1" @@ -1489,9 +1461,9 @@ JSONStream@^1.0.4, JSONStream@^1.3.4: through ">=2.2.7 <3" abab@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.2.tgz#a2fba1b122c69a85caa02d10f9270c7219709a9d" - integrity sha512-2scffjvioEmNz0OyDSLGWDfKCVwaKc6l9Pm9kOIREU13ClXZvHpg/nRL5xyjSSSLhOnXqft2HpsAzNEEA8cFFg== + version "2.0.3" + resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.3.tgz#623e2075e02eb2d3f2475e49f99c91846467907a" + integrity sha512-tsFzPpcttalNjFBCFMqsKYQcWxxen1pgJR56by//QwvJc4/OUS3kPOOttx2tSIfjsylB0pYu7f5D3K1RCxUnUg== abbrev@1: version "1.1.1" @@ -1522,9 +1494,9 @@ acorn@^5.5.3: integrity sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw== acorn@^6.0.1: - version "6.3.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.3.0.tgz#0087509119ffa4fc0a0041d1e93a417e68cb856e" - integrity sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA== + version "6.4.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.0.tgz#b659d2ffbafa24baf5db1cdbb2c94a983ecd2784" + integrity sha512-gac8OEcQ2Li1dxIEWGZzsp2BitJxwkwcOm0zHAJLcPJaVvm58FRnk6RkuLRpU1EujipU2ZFODv2P9DLMfnV8mw== acorn@^7.1.0: version "7.1.0" @@ -1903,6 +1875,13 @@ before-after-hook@^2.0.0: resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.1.0.tgz#b6c03487f44e24200dd30ca5e6a1979c5d2fb635" integrity sha512-IWIbu7pMqyw3EAJHzzHbWa85b6oud/yfKYg5rqB5hNE8CeMi3nX+2C2sj0HswfblST86hpVEOAb9x34NZd6P7A== +bindings@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" + integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== + dependencies: + file-uri-to-path "1.0.0" + bluebird@^3.5.1, bluebird@^3.5.3, bluebird@^3.5.5: version "3.7.0" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.0.tgz#56a6a886e03f6ae577cffedeb524f8f2450293cf" @@ -1958,10 +1937,10 @@ bs-logger@0.x: dependencies: fast-json-stable-stringify "2.x" -bser@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.0.tgz#65fc784bf7f87c009b973c12db6546902fa9c7b5" - integrity sha512-8zsjWrQkkBoLK6uxASk1nJ2SKv97ltiGDo6A3wA0/yRPz+CwmEyDo0hUrhIuukG2JHpAl3bvFIixw2/3Hi0DOg== +bser@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" + integrity sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ== dependencies: node-int64 "^0.4.0" @@ -2480,10 +2459,10 @@ conventional-recommended-bump@^5.0.0: meow "^4.0.0" q "^1.5.1" -convert-source-map@^1.1.0, convert-source-map@^1.4.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.6.0.tgz#51b537a8c43e0f04dec1993bffcdd504e758ac20" - integrity sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A== +convert-source-map@^1.4.0, convert-source-map@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442" + integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA== dependencies: safe-buffer "~5.1.1" @@ -2889,7 +2868,7 @@ debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9: dependencies: ms "2.0.0" -debug@^3.1.0, debug@^3.2.6: +debug@^3.1.0: version "3.2.6" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== @@ -2931,11 +2910,6 @@ dedent@0.7.0, dedent@^0.7.0: resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw= -deep-extend@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" - integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== - deep-is@~0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" @@ -3021,11 +2995,6 @@ detect-indent@^5.0.0: resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-5.0.0.tgz#3871cc0a6a002e8c3e5b3cf7f336264675f06b9d" integrity sha1-OHHMCmoALow+Wzz38zYmRnXwa50= -detect-libc@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" - integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= - detect-newline@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2" @@ -3223,9 +3192,9 @@ escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= escodegen@^1.9.1: - version "1.12.0" - resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.12.0.tgz#f763daf840af172bb3a2b6dd7219c0e17f7ff541" - integrity sha512-TuA+EhsanGcme5T3R0L80u4t8CpbXQjegRmf7+FPTJrtCTErXFeelblRgHQa1FofEzqYYJmJ/OqjTwREp9qgmg== + version "1.12.1" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.12.1.tgz#08770602a74ac34c7a90ca9229e7d51e379abc76" + integrity sha512-Q8t2YZ+0e0pc7NRVj3B4tSQ9rim1oi4Fh46k2xhJ2qOiEwhQfdjyEQddWdj7ZFaKmU+5104vn1qrcjEPWq+bgQ== dependencies: esprima "^3.1.3" estraverse "^4.2.0" @@ -3404,9 +3373,9 @@ eventemitter3@^3.1.0: integrity sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q== exec-sh@^0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.2.tgz#6738de2eb7c8e671d0366aea0b0db8c6f7d7391b" - integrity sha512-9sLAvzhI5nc8TpuQUh4ahMdCrWT00wPWz7j47/emR5+2qEfoZP5zzUXvx+vdx+H6ohhnsYC31iX04QLYJK8zTg== + version "0.3.4" + resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.4.tgz#3a018ceb526cc6f6df2bb504b2bfe8e3a4934ec5" + integrity sha512-sEFIkc61v75sWeOe72qyrqg2Qg0OuLESziUDk/O/z2qgS15y2gWVFrI6f2Qn/qw/0/NCfCEsmNA4zOjkwEZT1A== execa@0.11.0: version "0.11.0" @@ -3573,7 +3542,7 @@ fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0: resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I= -fast-levenshtein@~2.0.4, fast-levenshtein@~2.0.6: +fast-levenshtein@~2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= @@ -3586,11 +3555,11 @@ fastq@^1.6.0: reusify "^1.0.0" fb-watchman@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.0.tgz#54e9abf7dfa2f26cd9b1636c588c1afc05de5d58" - integrity sha1-VOmr99+i8mzZsWNsWIwa/AXeXVg= + version "2.0.1" + resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.1.tgz#fc84fb39d2709cf3ff6d743706157bb5708a8a85" + integrity sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg== dependencies: - bser "^2.0.0" + bser "2.1.1" figgy-pudding@^3.4.1, figgy-pudding@^3.5.1: version "3.5.1" @@ -3626,6 +3595,11 @@ file-entry-cache@^5.0.1: dependencies: flat-cache "^2.0.1" +file-uri-to-path@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" + integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== + fill-range@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" @@ -3784,12 +3758,12 @@ fs.realpath@^1.0.0: integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= fsevents@^1.2.7: - version "1.2.9" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.9.tgz#3f5ed66583ccd6f400b5a00db6f7e861363e388f" - integrity sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw== + version "1.2.11" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.11.tgz#67bf57f4758f02ede88fb2a1712fef4d15358be3" + integrity sha512-+ux3lx6peh0BpvY0JebGyZoiR4D+oYzdPZMKJwkZ+sFkNJzpL7tXc/wehS49gUAxg3tmMHPHZkA8JU2rhhgDHw== dependencies: + bindings "^1.5.0" nan "^2.12.1" - node-pre-gyp "^0.12.0" function-bind@^1.1.1: version "1.1.1" @@ -3967,7 +3941,7 @@ glob-to-regexp@^0.3.0: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab" integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs= -glob@*, glob@^7.1.6: +glob@*, glob@^7.1.2, glob@^7.1.6: version "7.1.6" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== @@ -3979,7 +3953,7 @@ glob@*, glob@^7.1.6: once "^1.3.0" path-is-absolute "^1.0.0" -glob@7.1.4, glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4: +glob@7.1.4, glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.3, glob@^7.1.4: version "7.1.4" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255" integrity sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A== @@ -4230,7 +4204,7 @@ husky@^3.0.9: run-node "^1.0.0" slash "^3.0.0" -iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4, iconv-lite@~0.4.13: +iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@~0.4.13: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== @@ -4323,7 +4297,7 @@ inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -ini@^1.3.2, ini@^1.3.4, ini@~1.3.0: +ini@^1.3.2, ini@^1.3.4: version "1.3.5" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== @@ -5485,7 +5459,7 @@ lodash.map@^4.5.1: resolved "https://registry.yarnpkg.com/lodash.map/-/lodash.map-4.6.0.tgz#771ec7839e3473d9c4cde28b19394c3562f4f6d3" integrity sha1-dx7Hg540c9nEzeKLGTlMNWL09tM= -lodash.memoize@4.x, lodash.memoize@^4.1.2: +lodash.memoize@4.x: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= @@ -5515,11 +5489,6 @@ lodash.templatesettings@^4.0.0: dependencies: lodash._reinterpolate "^3.0.0" -lodash.unescape@4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/lodash.unescape/-/lodash.unescape-4.0.1.tgz#bf2249886ce514cda112fae9218cdc065211fc9c" - integrity sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw= - lodash.uniq@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" @@ -5944,15 +5913,6 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= -needle@^2.2.1: - version "2.4.0" - resolved "https://registry.yarnpkg.com/needle/-/needle-2.4.0.tgz#6833e74975c444642590e15a750288c5f939b57c" - integrity sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg== - dependencies: - debug "^3.2.6" - iconv-lite "^0.4.4" - sax "^1.2.4" - neo-async@^2.6.0: version "2.6.1" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c" @@ -6023,22 +5983,6 @@ node-notifier@^5.4.2: shellwords "^0.1.1" which "^1.3.0" -node-pre-gyp@^0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.12.0.tgz#39ba4bb1439da030295f899e3b520b7785766149" - integrity sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A== - dependencies: - detect-libc "^1.0.2" - mkdirp "^0.5.1" - needle "^2.2.1" - nopt "^4.0.1" - npm-packlist "^1.1.6" - npmlog "^4.0.2" - rc "^1.2.7" - rimraf "^2.6.1" - semver "^5.3.0" - tar "^4" - "nopt@2 || 3": version "3.0.6" resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" @@ -6046,14 +5990,6 @@ node-pre-gyp@^0.12.0: dependencies: abbrev "1" -nopt@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d" - integrity sha1-0NRoWv1UFRk8jHUFYC0NF81kR00= - dependencies: - abbrev "1" - osenv "^0.1.4" - normalize-package-data@^2.0.0, normalize-package-data@^2.3.0, normalize-package-data@^2.3.2, normalize-package-data@^2.3.4, normalize-package-data@^2.3.5, normalize-package-data@^2.4.0, normalize-package-data@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" @@ -6110,7 +6046,7 @@ npm-lifecycle@^3.1.2: semver "^5.6.0" validate-npm-package-name "^3.0.0" -npm-packlist@^1.1.6, npm-packlist@^1.4.4: +npm-packlist@^1.4.4: version "1.4.4" resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.4.tgz#866224233850ac534b63d1a6e76050092b5d2f44" integrity sha512-zTLo8UcVYtDU3gdeaFu2Xu0n0EvelfHDGuqtNIn5RO7yQj4H1TqNdBc/yZjxnWA0PVB8D3Woyp0i5B43JwQ6Vw== @@ -6141,7 +6077,7 @@ npm-run-path@^3.0.0: dependencies: path-key "^3.0.0" -"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.2, npmlog@^4.1.2: +"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== @@ -6157,9 +6093,9 @@ number-is-nan@^1.0.0: integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= nwsapi@^2.0.7: - version "2.1.4" - resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.1.4.tgz#e006a878db23636f8e8a67d33ca0e4edf61a842f" - integrity sha512-iGfd9Y6SFdTNldEy2L0GUhcarIutFmk+MPWIn9dmj8NMIup03G08uUF2KGbbmv/Ux4RT0VZJoP/sVbWA6d/VIw== + version "2.2.0" + resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7" + integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ== oauth-sign@~0.9.0: version "0.9.0" @@ -6261,19 +6197,7 @@ optimist@^0.6.1: minimist "~0.0.1" wordwrap "~0.0.2" -optionator@^0.8.1: - version "0.8.2" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64" - integrity sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q= - dependencies: - deep-is "~0.1.3" - fast-levenshtein "~2.0.4" - levn "~0.3.0" - prelude-ls "~1.1.2" - type-check "~0.3.2" - wordwrap "~1.0.0" - -optionator@^0.8.3: +optionator@^0.8.1, optionator@^0.8.3: version "0.8.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== @@ -6303,7 +6227,7 @@ os-tmpdir@^1.0.0, os-tmpdir@~1.0.2: resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= -osenv@^0.1.4, osenv@^0.1.5: +osenv@^0.1.5: version "0.1.5" resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410" integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g== @@ -6696,9 +6620,9 @@ promise-retry@^1.1.1: retry "^0.10.0" prompts@^2.0.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.2.1.tgz#f901dd2a2dfee080359c0e20059b24188d75ad35" - integrity sha512-VObPvJiWPhpZI6C5m60XOzTfnYg/xc/an+r9VYymj9WJW3B/DIH+REzjpAACPf8brwPeP+7vz3bIim3S+AaMjw== + version "2.3.0" + resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.3.0.tgz#a444e968fa4cc7e86689a74050685ac8006c4cc4" + integrity sha512-NfbbPPg/74fT7wk2XYQ7hAIp9zJyZp5Fu19iRbORqqy1BhtrkZ0fPafBU+7bmn8ie69DpT0R6QpJIN2oisYjJg== dependencies: kleur "^3.0.3" sisteransi "^1.0.3" @@ -6727,11 +6651,16 @@ protoduck@^5.0.1: dependencies: genfun "^5.0.0" -psl@^1.1.24, psl@^1.1.28: +psl@^1.1.24: version "1.4.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.4.0.tgz#5dd26156cdb69fa1fdb8ab1991667d3f80ced7c2" integrity sha512-HZzqCGPecFLyoRj5HLfuDSKYTJkAfB5thKBIkRHtGjWwY7p1dAyveIbXIq4tO0KYfDF2tHqPUgY9SDnGm00uFw== +psl@^1.1.28: + version "1.7.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.7.0.tgz#f1c4c47a8ef97167dea5d6bbf4816d736e884a3c" + integrity sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ== + pump@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" @@ -6782,16 +6711,6 @@ quick-lru@^1.0.0: resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-1.1.0.tgz#4360b17c61136ad38078397ff11416e186dcfbb8" integrity sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g= -rc@^1.2.7: - version "1.2.8" - resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" - integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== - dependencies: - deep-extend "^0.6.0" - ini "~1.3.0" - minimist "^1.2.0" - strip-json-comments "~2.0.1" - react-is@^16.8.4: version "16.10.2" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.10.2.tgz#984120fd4d16800e9a738208ab1fba422d23b5ab" @@ -7018,19 +6937,19 @@ repeating@^2.0.0: dependencies: is-finite "^1.0.0" -request-promise-core@1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.2.tgz#339f6aababcafdb31c799ff158700336301d3346" - integrity sha512-UHYyq1MO8GsefGEt7EprS8UrXsm1TxEvFUX1IMTuSLU2Rh7fTIdFtl8xD7JiEYiWU2dl+NYAjCTksTehQUxPag== +request-promise-core@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.3.tgz#e9a3c081b51380dfea677336061fea879a829ee9" + integrity sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ== dependencies: - lodash "^4.17.11" + lodash "^4.17.15" request-promise-native@^1.0.5: - version "1.0.7" - resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.7.tgz#a49868a624bdea5069f1251d0a836e0d89aa2c59" - integrity sha512-rIMnbBdgNViL37nZ1b3L/VfPOpSi0TqVDQPAvO6U14lMzOLrt5nilxCQqtDKhZeDiW0/hkCXGoQjhgJd/tCh6w== + version "1.0.8" + resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.8.tgz#a455b960b826e44e2bf8999af64dff2bfe58cb36" + integrity sha512-dapwLGqkHtwL5AEbfenuzjTYg35Jd6KPytsC2/TLkVMz8rm+tNt72MGUWT1RP/aYawMpN6HqbNGBQaRcBtjQMQ== dependencies: - request-promise-core "1.1.2" + request-promise-core "1.1.3" stealthy-require "^1.1.1" tough-cookie "^2.3.3" @@ -7160,7 +7079,7 @@ right-pad@^1.0.1: resolved "https://registry.yarnpkg.com/right-pad/-/right-pad-1.0.1.tgz#8ca08c2cbb5b55e74dafa96bf7fd1a27d568c8d0" integrity sha1-jKCMLLtbVedNr6lr9/0aJ9VoyNA= -rimraf@2, rimraf@^2.5.2, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@^2.6.3: +rimraf@2, rimraf@^2.5.2, rimraf@^2.5.4, rimraf@^2.6.2, rimraf@^2.6.3: version "2.7.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== @@ -7331,9 +7250,9 @@ signal-exit@^3.0.0, signal-exit@^3.0.2: integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= sisteransi@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.3.tgz#98168d62b79e3a5e758e27ae63c4a053d748f4eb" - integrity sha512-SbEG75TzH8G7eVXFSN5f9EExILKfly7SUvVY5DhhYLvfhKqhDFY0OzevWa/zwak0RLRfWS5AvfMWpd9gJvr5Yg== + version "1.0.4" + resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.4.tgz#386713f1ef688c7c0304dc4c0632898941cad2e3" + integrity sha512-/ekMoM4NJ59ivGSfKapeG+FWtrmWvA1p6FBZwXrqojw90vJu8lBmrTxCMuBCydKtkaUe2zt4PlxeTKpjwMbyig== slash@^1.0.0: version "1.0.0" @@ -7725,11 +7644,6 @@ strip-json-comments@3.0.1, strip-json-comments@^3.0.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.0.1.tgz#85713975a91fb87bf1b305cca77395e40d2a64a7" integrity sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw== -strip-json-comments@~2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" - integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= - strong-log-transformer@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/strong-log-transformer/-/strong-log-transformer-2.1.0.tgz#0f5ed78d325e0421ac6f90f7f10e691d6ae3ae10" @@ -7785,7 +7699,7 @@ table@^5.2.3: slice-ansi "^2.1.0" string-width "^3.0.0" -tar@^4, tar@^4.4.10, tar@^4.4.12, tar@^4.4.8: +tar@^4.4.10, tar@^4.4.12, tar@^4.4.8: version "4.4.13" resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.13.tgz#43b364bc52888d555298637b10d60790254ab525" integrity sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA== @@ -8336,11 +8250,6 @@ wordwrap@~0.0.2: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" integrity sha1-o9XabNXAvAAI03I0u68b7WMFkQc= -wordwrap@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" - integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= - wrap-ansi@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-3.0.1.tgz#288a04d87eda5c286e060dfe8f135ce8d007f8ba"