diff --git a/README.md b/README.md index d8bc576a1ef5..6900259d2784 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ One advantage is there is no tooling required to reconcile differences between A Palantir, the backers behind TSLint announced earlier this year that **they would be deprecating TSLint in favor of supporting `typescript-eslint`** in order to benefit the community. You can read more about that here: https://medium.com/palantir/tslint-in-2019-1a144c2317a9 -The TypeScript Team themselves also announced their plans to move the TypeScript codebase from TSLint to `typescript-eslint`, and they have been big supporters of this project. +The TypeScript Team themselves also announced their plans to move the TypeScript codebase from TSLint to `typescript-eslint`, and they have been big supporters of this project. More details at https://github.com/microsoft/TypeScript/issues/30553
diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index d5163109b1e6..c436e32be6c1 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -145,7 +145,7 @@ Then you should add `airbnb` (or `airbnb-base`) to your `extends` section of `.e | [`@typescript-eslint/no-array-constructor`](./docs/rules/no-array-constructor.md) | Disallow generic `Array` constructors | :heavy_check_mark: | :wrench: | | | [`@typescript-eslint/no-empty-function`](./docs/rules/no-empty-function.md) | Disallow empty functions | | | | | [`@typescript-eslint/no-empty-interface`](./docs/rules/no-empty-interface.md) | Disallow the declaration of empty interfaces | :heavy_check_mark: | | | -| [`@typescript-eslint/no-explicit-any`](./docs/rules/no-explicit-any.md) | Disallow usage of the `any` type | :heavy_check_mark: | | | +| [`@typescript-eslint/no-explicit-any`](./docs/rules/no-explicit-any.md) | Disallow usage of the `any` type | :heavy_check_mark: | :wrench: | | | [`@typescript-eslint/no-extra-parens`](./docs/rules/no-extra-parens.md) | Disallow unnecessary parentheses | | :wrench: | | | [`@typescript-eslint/no-extraneous-class`](./docs/rules/no-extraneous-class.md) | Forbids the use of classes as namespaces | | | | | [`@typescript-eslint/no-floating-promises`](./docs/rules/no-floating-promises.md) | Requires Promise-like values to be handled appropriately. | | | :thought_balloon: | @@ -159,7 +159,6 @@ Then you should add `airbnb` (or `airbnb-base`) to your `extends` section of `.e | [`@typescript-eslint/no-parameter-properties`](./docs/rules/no-parameter-properties.md) | Disallow the use of parameter properties in class constructors | :heavy_check_mark: | | | | [`@typescript-eslint/no-require-imports`](./docs/rules/no-require-imports.md) | Disallows invocation of `require()` | | | | | [`@typescript-eslint/no-this-alias`](./docs/rules/no-this-alias.md) | Disallow aliasing `this` | | | | -| [`@typescript-eslint/no-triple-slash-reference`](./docs/rules/no-triple-slash-reference.md) | Disallow `/// ` comments | :heavy_check_mark: | | | | [`@typescript-eslint/no-type-alias`](./docs/rules/no-type-alias.md) | Disallow the use of type aliases | | | | | [`@typescript-eslint/no-unnecessary-qualifier`](./docs/rules/no-unnecessary-qualifier.md) | Warns when a namespace qualifier is unnecessary | | :wrench: | :thought_balloon: | | [`@typescript-eslint/no-unnecessary-type-assertion`](./docs/rules/no-unnecessary-type-assertion.md) | Warns if a type assertion does not change the type of an expression | | :wrench: | :thought_balloon: | @@ -178,6 +177,8 @@ Then you should add `airbnb` (or `airbnb-base`) to your `extends` section of `.e | [`@typescript-eslint/require-array-sort-compare`](./docs/rules/require-array-sort-compare.md) | Enforce giving `compare` argument to `Array#sort` | | | :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/semi`](./docs/rules/semi.md) | Require or disallow semicolons instead of ASI | | :wrench: | | +| [`@typescript-eslint/strict-boolean-expressions`](./docs/rules/strict-boolean-expressions.md) | Restricts the types allowed in boolean expressions | | | :thought_balloon: | +| [`@typescript-eslint/triple-slash-reference`](./docs/rules/triple-slash-reference.md) | Sets preference level for triple slash directives versus ES6-style import declarations | | | | | [`@typescript-eslint/type-annotation-spacing`](./docs/rules/type-annotation-spacing.md) | Require consistent spacing around type annotations | :heavy_check_mark: | :wrench: | | | [`@typescript-eslint/unbound-method`](./docs/rules/unbound-method.md) | Enforces unbound methods are called with their expected scope | | | :thought_balloon: | | [`@typescript-eslint/unified-signatures`](./docs/rules/unified-signatures.md) | Warns for any two overloads that could be unified into one by using a union or an optional/rest parameter | | | | diff --git a/packages/eslint-plugin/ROADMAP.md b/packages/eslint-plugin/ROADMAP.md index 8a527cd33132..60cdbe7de24f 100644 --- a/packages/eslint-plugin/ROADMAP.md +++ b/packages/eslint-plugin/ROADMAP.md @@ -92,7 +92,7 @@ | [`prefer-object-spread`] | 🌟 | [`prefer-object-spread`][prefer-object-spread] | | [`radix`] | 🌟 | [`radix`][radix] | | [`restrict-plus-operands`] | ✅ | [`@typescript-eslint/restrict-plus-operands`] | -| [`strict-boolean-expressions`] | 🛑 | N/A | +| [`strict-boolean-expressions`] | ✅ | [`@typescript-eslint/strict-boolean-expressions`] | | [`strict-type-predicates`] | 🛑 | N/A | | [`switch-default`] | 🌟 | [`default-case`][default-case] | | [`triple-equals`] | 🌟 | [`eqeqeq`][eqeqeq] | diff --git a/packages/eslint-plugin/docs/rules/array-type.md b/packages/eslint-plugin/docs/rules/array-type.md index c7e68a76505f..8906f0c9a4c2 100644 --- a/packages/eslint-plugin/docs/rules/array-type.md +++ b/packages/eslint-plugin/docs/rules/array-type.md @@ -1,33 +1,83 @@ # Requires using either `T[]` or `Array` for arrays (array-type) -```ts -class Foo>> extends Bar> - implements Baz> { - private s: Array; - - constructor(p: Array) { - return new Array(); - } -} -``` +Using the same style for array definitions across your codebase makes it easier for your developers to read and understand the types. ## Rule Details -This rule aims to standardise usage of array. +This rule aims to standardise usage of array types within your codebase. ## Options -Default config: +This rule accepts one option - a single string + +- `"array"` enforces use of `T[]` for all types `T`. +- `"generic"` enforces use of `Array` for all types `T`. +- `"array-simple"` enforces use of `T[]` if `T` is a simple type. + +Without providing an option, by default the rule will enforce `"array"`. + +### `"array"` + +Always use `T[]` or `readonly T[]` for all array types. + +Incorrect code for `"array"`: + +```ts +const x: Array = ['a', 'b']; +const y: ReadonlyArray = ['a', 'b']; +``` -```JSON -{ - "array-type": ["error", { "default": "array", "readonly": "array" }] -} +Correct code for `"array"`: + +```ts +const x: string[] = ['a', 'b']; +const y: readonly string[] = ['a', 'b']; ``` -- `array` enforces use of `T[]` for all types `T`. -- `generic` enforces use of `Array` for all types `T`. -- `array-simple` enforces use of `T[]` if `T` is a simple type. +### `"generic"` + +Always use `Array` or `ReadonlyArray` for all array types. + +Incorrect code for `"generic"`: + +```ts +const x: string[] = ['a', 'b']; +const y: readonly string[] = ['a', 'b']; +``` + +Correct code for `"generic"`: + +```ts +const x: Array = ['a', 'b']; +const y: ReadonlyArray = ['a', 'b']; +``` + +### `"array-simple"` + +Use `T[]` or `readonly T[]` for simple types (i.e. types which are just primitive names or type references). +Use `Array` or `ReadonlyArray` for all other types (union types, intersection types, object types, function types, etc). + +Incorrect code for `"array-simple"`: + +```ts +const a: (string | number)[] = ['a', 'b']; +const b: ({ prop: string })[] = [{ prop: 'a' }]; +const c: (() => void)[] = [() => {}]; +const d: Array = ['a', 'b']; +const e: Array = ['a', 'b']; +const f: ReadonlyArray = ['a', 'b']; +``` + +Correct code for `"array-simple"`: + +```ts +const a: Array = ['a', 'b']; +const b: Array<{ prop: string }> = [{ prop: 'a' }]; +const c: Array<() => void> = [() => {}]; +const d: MyType[] = ['a', 'b']; +const e: string[] = ['a', 'b']; +const f: readonly string[] = ['a', 'b']; +``` ## Related to diff --git a/packages/eslint-plugin/docs/rules/no-explicit-any.md b/packages/eslint-plugin/docs/rules/no-explicit-any.md index 3e3a6391a6f8..ccec9e952313 100644 --- a/packages/eslint-plugin/docs/rules/no-explicit-any.md +++ b/packages/eslint-plugin/docs/rules/no-explicit-any.md @@ -87,6 +87,24 @@ function greet(param: Array): string {} function greet(param: Array): Array {} ``` +## Options + +The rule accepts an options object with the following properties: + +```ts +type Options = { + // if true, auto-fixing will be made available in which the "any" type is converted to an "unknown" type + fixToUnknown: boolean; + // specify if arrays from the rest operator are considered okay + ignoreRestArgs: boolean; +}; + +const defaults = { + fixToUnknown: false, + ignoreRestArgs: false, +}; +``` + ### ignoreRestArgs A boolean to specify if arrays from the rest operator are considered okay. `false` by default. diff --git a/packages/eslint-plugin/docs/rules/no-triple-slash-reference.md b/packages/eslint-plugin/docs/rules/no-triple-slash-reference.md index be090acea265..409ff6093ca1 100644 --- a/packages/eslint-plugin/docs/rules/no-triple-slash-reference.md +++ b/packages/eslint-plugin/docs/rules/no-triple-slash-reference.md @@ -12,6 +12,8 @@ A triple-slash reference directive is a comment beginning with three slashes fol ES6 Modules handle this now: `import animal from "./Animal"` +## DEPRECATED - this rule has been deprecated in favour of [`triple-slash-reference`](./triple-slash-reference.md) + ## Rule Details Does not allow the use of `/// ` comments. diff --git a/packages/eslint-plugin/docs/rules/strict-boolean-expressions.md b/packages/eslint-plugin/docs/rules/strict-boolean-expressions.md new file mode 100644 index 000000000000..699af9baa431 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/strict-boolean-expressions.md @@ -0,0 +1,57 @@ +# Boolean expressions are limited to booleans (strict-boolean-expressions) + +Requires that any boolean expression is limited to true booleans rather than +casting another primitive to a boolean at runtime. + +It is useful to be explicit, for example, if you were trying to check if a +number was defined. Doing `if (number)` would evaluate to `false` if `number` +was defined and `0`. This rule forces these expressions to be explicit and to +strictly use booleans. + +The following nodes are checked: + +- Arguments to the `!`, `&&`, and `||` operators +- The condition in a conditional expression `(cond ? x : y)` +- Conditions for `if`, `for`, `while`, and `do-while` statements. + +Examples of **incorrect** code for this rule: + +```ts +const number = 0; +if (number) { + return; +} + +let foo = bar || 'foobar'; + +let undefinedItem; +let foo = undefinedItem ? 'foo' : 'bar'; + +let str = 'foo'; +while (str) { + break; +} +``` + +Examples of **correct** code for this rule: + +```ts +const number = 0; +if (typeof number !== 'undefined') { + return; +} + +let foo = typeof bar !== 'undefined' ? bar : 'foobar'; + +let undefinedItem; +let foo = typeof undefinedItem !== 'undefined' ? 'foo' : 'bar'; + +let str = 'foo'; +while (typeof str !== 'undefined') { + break; +} +``` + +## Related To + +- TSLint: [strict-boolean-expressions](https://palantir.github.io/tslint/rules/strict-boolean-expressions) diff --git a/packages/eslint-plugin/docs/rules/triple-slash-reference.md b/packages/eslint-plugin/docs/rules/triple-slash-reference.md new file mode 100644 index 000000000000..3dccaa925e9b --- /dev/null +++ b/packages/eslint-plugin/docs/rules/triple-slash-reference.md @@ -0,0 +1,58 @@ +# Sets preference level for triple slash directives versus ES6-style import declarations. (triple-slash-reference) + +Use of triple-slash reference type directives is discouraged in favor of the newer `import` style. This rule allows you to ban use of `/// `, `/// `, or `/// ` directives. + +Consider using this rule in place of [`no-triple-slash-reference`](./no-triple-slash-reference.md) which has been deprecated. + +## Rule Details + +With `{ "path": "never", "types": "never", "lib": "never" }` options set, the following will all be **incorrect** usage: + +```ts +/// +/// +/// +``` + +Examples of **incorrect** code for the `{ "types": "prefer-import" }` option. Note that these are only errors when **both** stlyes are used for the **same** module: + +```ts +/// +import * as foo from 'foo'; +``` + +```ts +/// +import foo = require('foo'); +``` + +With `{ "path": "always", "types": "always", "lib": "always" }` options set, the following will all be **correct** usage: + +```ts +/// +/// +/// +``` + +Examples of **correct** code for the `{ "types": "prefer-import" }` option: + +```ts +import * as foo from 'foo'; +``` + +```ts +import foo = require('foo'); +``` + +## When To Use It + +If you want to ban use of one or all of the triple slash reference directives, or any time you might use triple-slash type reference directives and ES6 import declarations in the same file. + +## When Not To Use It + +If you want to use all flavors of triple slash reference directives. + +## Compatibility + +- TSLint: [no-reference](http://palantir.github.io/tslint/rules/no-reference/) +- TSLint: [no-reference-import](https://palantir.github.io/tslint/rules/no-reference-import/) diff --git a/packages/eslint-plugin/src/configs/all.json b/packages/eslint-plugin/src/configs/all.json index 7dc471fdd5d0..cd5b2bb9cbe9 100644 --- a/packages/eslint-plugin/src/configs/all.json +++ b/packages/eslint-plugin/src/configs/all.json @@ -42,7 +42,6 @@ "@typescript-eslint/no-parameter-properties": "error", "@typescript-eslint/no-require-imports": "error", "@typescript-eslint/no-this-alias": "error", - "@typescript-eslint/no-triple-slash-reference": "error", "@typescript-eslint/no-type-alias": "error", "@typescript-eslint/no-unnecessary-qualifier": "error", "@typescript-eslint/no-unnecessary-type-assertion": "error", @@ -65,6 +64,7 @@ "@typescript-eslint/restrict-plus-operands": "error", "semi": "off", "@typescript-eslint/semi": "error", + "@typescript-eslint/triple-slash-reference": "error", "@typescript-eslint/type-annotation-spacing": "error", "@typescript-eslint/unbound-method": "error", "@typescript-eslint/unified-signatures": "error" diff --git a/packages/eslint-plugin/src/rules/ban-types.ts b/packages/eslint-plugin/src/rules/ban-types.ts index 059f73020249..d0a57daf2335 100644 --- a/packages/eslint-plugin/src/rules/ban-types.ts +++ b/packages/eslint-plugin/src/rules/ban-types.ts @@ -1,8 +1,4 @@ -import { - TSESLint, - TSESTree, - AST_NODE_TYPES, -} from '@typescript-eslint/experimental-utils'; +import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils'; import * as util from '../util'; type Options = [ @@ -20,6 +16,31 @@ type Options = [ ]; type MessageIds = 'bannedTypeMessage'; +function stringifyTypeName( + node: TSESTree.EntityName, + sourceCode: TSESLint.SourceCode, +): string { + return sourceCode.getText(node).replace(/ /g, ''); +} + +function getCustomMessage( + bannedType: null | string | { message?: string; fixWith?: string }, +) { + if (bannedType === null) { + return ''; + } + + if (typeof bannedType === 'string') { + return ` ${bannedType}`; + } + + if (bannedType.message) { + return ` ${bannedType.message}`; + } + + return ''; +} + export default util.createRule({ name: 'ban-types', meta: { @@ -87,39 +108,23 @@ export default util.createRule({ ], create(context, [{ types: bannedTypes }]) { return { - 'TSTypeReference Identifier'(node: TSESTree.Identifier) { - if ( - node.parent && - node.parent.type !== AST_NODE_TYPES.TSQualifiedName - ) { - if (node.name in bannedTypes) { - let customMessage = ''; - const bannedCfgValue = bannedTypes[node.name]; + TSTypeReference({ typeName }) { + const name = stringifyTypeName(typeName, context.getSourceCode()); - let fix: TSESLint.ReportFixFunction | null = null; + if (name in bannedTypes) { + const bannedType = bannedTypes[name]; + const customMessage = getCustomMessage(bannedType); + const fixWith = bannedType && (bannedType as any).fixWith; - if (typeof bannedCfgValue === 'string') { - customMessage += ` ${bannedCfgValue}`; - } else if (bannedCfgValue !== null) { - if (bannedCfgValue.message) { - customMessage += ` ${bannedCfgValue.message}`; - } - if (bannedCfgValue.fixWith) { - const fixWith = bannedCfgValue.fixWith; - fix = fixer => fixer.replaceText(node, fixWith); - } - } - - context.report({ - node, - messageId: 'bannedTypeMessage', - data: { - name: node.name, - customMessage, - }, - fix, - }); - } + context.report({ + node: typeName, + messageId: 'bannedTypeMessage', + data: { + name: name, + customMessage, + }, + fix: fixWith ? fixer => fixer.replaceText(typeName, fixWith) : null, + }); } }, }; diff --git a/packages/eslint-plugin/src/rules/consistent-type-definitions.ts b/packages/eslint-plugin/src/rules/consistent-type-definitions.ts index d8d9bb675a21..eea12b9cf845 100644 --- a/packages/eslint-plugin/src/rules/consistent-type-definitions.ts +++ b/packages/eslint-plugin/src/rules/consistent-type-definitions.ts @@ -27,7 +27,6 @@ export default util.createRule({ const sourceCode = context.getSourceCode(); return { - // VariableDeclaration with kind type has only one VariableDeclarator "TSTypeAliasDeclaration[typeAnnotation.type='TSTypeLiteral']"( node: TSESTree.TSTypeAliasDeclaration, ) { diff --git a/packages/eslint-plugin/src/rules/indent-new-do-not-use/index.ts b/packages/eslint-plugin/src/rules/indent-new-do-not-use/index.ts index 14088c553789..70cf1c37919a 100644 --- a/packages/eslint-plugin/src/rules/indent-new-do-not-use/index.ts +++ b/packages/eslint-plugin/src/rules/indent-new-do-not-use/index.ts @@ -1382,6 +1382,10 @@ export default createRule({ }, VariableDeclaration(node) { + if (node.declarations.length === 0) { + return; + } + let variableIndent = Object.prototype.hasOwnProperty.call( options.VariableDeclarator, node.kind, diff --git a/packages/eslint-plugin/src/rules/indent.ts b/packages/eslint-plugin/src/rules/indent.ts index 6b2a83066dd4..321f32f4cceb 100644 --- a/packages/eslint-plugin/src/rules/indent.ts +++ b/packages/eslint-plugin/src/rules/indent.ts @@ -175,6 +175,15 @@ export default util.createRule({ } }, + VariableDeclaration(node: TSESTree.VariableDeclaration) { + // https://github.com/typescript-eslint/typescript-eslint/issues/441 + if (node.declarations.length === 0) { + return; + } + + return rules.VariableDeclaration(node); + }, + TSAsExpression(node: TSESTree.TSAsExpression) { // transform it to a BinaryExpression return rules['BinaryExpression, LogicalExpression']({ diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index 5d6fc99aa486..00107aa57a8d 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -53,6 +53,8 @@ import promiseFunctionAsync from './promise-function-async'; import requireArraySortCompare from './require-array-sort-compare'; import restrictPlusOperands from './restrict-plus-operands'; import semi from './semi'; +import strictBooleanExpressions from './strict-boolean-expressions'; +import tripleSlashReference from './triple-slash-reference'; import typeAnnotationSpacing from './type-annotation-spacing'; import unboundMethod from './unbound-method'; import unifiedSignatures from './unified-signatures'; @@ -113,6 +115,8 @@ export default { 'require-array-sort-compare': requireArraySortCompare, 'restrict-plus-operands': restrictPlusOperands, semi: semi, + 'strict-boolean-expressions': strictBooleanExpressions, + 'triple-slash-reference': tripleSlashReference, 'type-annotation-spacing': typeAnnotationSpacing, 'unbound-method': unboundMethod, 'unified-signatures': unifiedSignatures, diff --git a/packages/eslint-plugin/src/rules/no-explicit-any.ts b/packages/eslint-plugin/src/rules/no-explicit-any.ts index c27b4ab59923..f5a555933615 100644 --- a/packages/eslint-plugin/src/rules/no-explicit-any.ts +++ b/packages/eslint-plugin/src/rules/no-explicit-any.ts @@ -3,8 +3,17 @@ import { AST_NODE_TYPES, } from '@typescript-eslint/experimental-utils'; import * as util from '../util'; +import { TSESLint } from '@typescript-eslint/experimental-utils'; -export default util.createRule({ +export type Options = [ + { + fixToUnknown?: boolean; + ignoreRestArgs?: boolean; + } +]; +export type MessageIds = 'unexpectedAny'; + +export default util.createRule({ name: 'no-explicit-any', meta: { type: 'suggestion', @@ -13,6 +22,7 @@ export default util.createRule({ category: 'Best Practices', recommended: 'warn', }, + fixable: 'code', messages: { unexpectedAny: 'Unexpected any. Specify a different type.', }, @@ -21,6 +31,9 @@ export default util.createRule({ type: 'object', additionalProperties: false, properties: { + fixToUnknown: { + type: 'boolean', + }, ignoreRestArgs: { type: 'boolean', }, @@ -30,10 +43,11 @@ export default util.createRule({ }, defaultOptions: [ { + fixToUnknown: false, ignoreRestArgs: false, }, ], - create(context, [{ ignoreRestArgs }]) { + create(context, [{ ignoreRestArgs, fixToUnknown }]) { /** * Checks if the node is an arrow function, function declaration or function expression * @param node the node to be validated. @@ -155,9 +169,17 @@ export default util.createRule({ if (ignoreRestArgs && isNodeDescendantOfRestElementInFunction(node)) { return; } + + let fix: TSESLint.ReportFixFunction | null = null; + + if (fixToUnknown) { + fix = fixer => fixer.replaceText(node, 'unknown'); + } + context.report({ node, messageId: 'unexpectedAny', + fix, }); }, }; diff --git a/packages/eslint-plugin/src/rules/no-triple-slash-reference.ts b/packages/eslint-plugin/src/rules/no-triple-slash-reference.ts index c7780a99bf38..49f164201664 100644 --- a/packages/eslint-plugin/src/rules/no-triple-slash-reference.ts +++ b/packages/eslint-plugin/src/rules/no-triple-slash-reference.ts @@ -10,8 +10,10 @@ export default util.createRule({ recommended: 'error', }, schema: [], + deprecated: true, + replacedBy: ['triple-slash-reference'], messages: { - tripleSlashReference: 'Do not use a triple slash reference.', + noTripleSlashReference: 'Do not use a triple slash reference.', }, }, defaultOptions: [], @@ -30,7 +32,7 @@ export default util.createRule({ if (referenceRegExp.test(comment.value)) { context.report({ node: comment, - messageId: 'tripleSlashReference', + messageId: 'noTripleSlashReference', }); } }); diff --git a/packages/eslint-plugin/src/rules/prefer-for-of.ts b/packages/eslint-plugin/src/rules/prefer-for-of.ts index 19551a911664..89f6c5403032 100644 --- a/packages/eslint-plugin/src/rules/prefer-for-of.ts +++ b/packages/eslint-plugin/src/rules/prefer-for-of.ts @@ -186,8 +186,11 @@ export default util.createRule({ return; } - const [declarator] = node.init.declarations; + const declarator = node.init.declarations[0] as + | TSESTree.VariableDeclarator + | undefined; if ( + !declarator || !isZeroInitialized(declarator) || declarator.id.type !== AST_NODE_TYPES.Identifier ) { diff --git a/packages/eslint-plugin/src/rules/strict-boolean-expressions.ts b/packages/eslint-plugin/src/rules/strict-boolean-expressions.ts new file mode 100644 index 000000000000..5118b46a2486 --- /dev/null +++ b/packages/eslint-plugin/src/rules/strict-boolean-expressions.ts @@ -0,0 +1,101 @@ +import { + TSESTree, + AST_NODE_TYPES, +} from '@typescript-eslint/experimental-utils'; +import ts from 'typescript'; +import * as tsutils from 'tsutils'; +import * as util from '../util'; + +type ExpressionWithTest = + | TSESTree.ConditionalExpression + | TSESTree.DoWhileStatement + | TSESTree.ForStatement + | TSESTree.IfStatement + | TSESTree.WhileStatement; + +export default util.createRule({ + name: 'strict-boolean-expressions', + meta: { + type: 'suggestion', + docs: { + description: 'Restricts the types allowed in boolean expressions', + category: 'Best Practices', + recommended: false, + }, + schema: [], + messages: { + strictBooleanExpression: 'Unexpected non-boolean in conditional.', + }, + }, + defaultOptions: [], + create(context) { + const service = util.getParserServices(context); + const checker = service.program.getTypeChecker(); + + /** + * Determines if the node has a boolean type. + */ + function isBooleanType(node: TSESTree.Node): boolean { + const tsNode = service.esTreeNodeToTSNodeMap.get( + node, + ); + const type = util.getConstrainedTypeAtLocation(checker, tsNode); + return tsutils.isTypeFlagSet(type, ts.TypeFlags.BooleanLike); + } + + /** + * Asserts that a testable expression contains a boolean, reports otherwise. + * Filters all LogicalExpressions to prevent some duplicate reports. + */ + function assertTestExpressionContainsBoolean( + node: ExpressionWithTest, + ): void { + if ( + node.test !== null && + node.test.type !== AST_NODE_TYPES.LogicalExpression && + !isBooleanType(node.test) + ) { + reportNode(node.test); + } + } + + /** + * Asserts that a logical expression contains a boolean, reports otherwise. + */ + function assertLocalExpressionContainsBoolean( + node: TSESTree.LogicalExpression, + ): void { + if (!isBooleanType(node.left) || !isBooleanType(node.right)) { + reportNode(node); + } + } + + /** + * Asserts that a unary expression contains a boolean, reports otherwise. + */ + function assertUnaryExpressionContainsBoolean( + node: TSESTree.UnaryExpression, + ): void { + if (!isBooleanType(node.argument)) { + reportNode(node.argument); + } + } + + /** + * Reports an offending node in context. + */ + function reportNode(node: TSESTree.Node): void { + context.report({ node, messageId: 'strictBooleanExpression' }); + } + + return { + ConditionalExpression: assertTestExpressionContainsBoolean, + DoWhileStatement: assertTestExpressionContainsBoolean, + ForStatement: assertTestExpressionContainsBoolean, + IfStatement: assertTestExpressionContainsBoolean, + WhileStatement: assertTestExpressionContainsBoolean, + LogicalExpression: assertLocalExpressionContainsBoolean, + 'UnaryExpression[operator="!"]': assertUnaryExpressionContainsBoolean, + }; + }, +}); diff --git a/packages/eslint-plugin/src/rules/triple-slash-reference.ts b/packages/eslint-plugin/src/rules/triple-slash-reference.ts new file mode 100644 index 000000000000..286f445c9661 --- /dev/null +++ b/packages/eslint-plugin/src/rules/triple-slash-reference.ts @@ -0,0 +1,129 @@ +import * as util from '../util'; +import { + Literal, + Node, + TSExternalModuleReference, +} from '@typescript-eslint/typescript-estree/dist/ts-estree/ts-estree'; +import { TSESTree } from '@typescript-eslint/typescript-estree'; + +type Options = [ + { + lib?: 'always' | 'never'; + path?: 'always' | 'never'; + types?: 'always' | 'never' | 'prefer-import'; + } +]; +type MessageIds = 'tripleSlashReference'; + +export default util.createRule({ + name: 'triple-slash-reference', + meta: { + type: 'suggestion', + docs: { + description: + 'Sets preference level for triple slash directives versus ES6-style import declarations', + category: 'Best Practices', + recommended: false, + }, + messages: { + tripleSlashReference: + 'Do not use a triple slash reference for {{module}}, use `import` style instead.', + }, + schema: [ + { + type: 'object', + properties: { + lib: { + enum: ['always', 'never'], + }, + path: { + enum: ['always', 'never'], + }, + types: { + enum: ['always', 'never', 'prefer-import'], + }, + }, + additionalProperties: false, + }, + ], + }, + defaultOptions: [ + { + lib: 'always', + path: 'never', + types: 'prefer-import', + }, + ], + create(context, [{ lib, path, types }]) { + let programNode: Node; + const sourceCode = context.getSourceCode(); + const references: ({ + comment: TSESTree.Comment; + importName: string; + })[] = []; + + function hasMatchingReference(source: Literal) { + references.forEach(reference => { + if (reference.importName === source.value) { + context.report({ + node: reference.comment, + messageId: 'tripleSlashReference', + data: { + module: reference.importName, + }, + }); + } + }); + } + return { + ImportDeclaration(node) { + if (programNode) { + const source = node.source as Literal; + hasMatchingReference(source); + } + }, + TSImportEqualsDeclaration(node) { + if (programNode) { + const source = (node.moduleReference as TSExternalModuleReference) + .expression as Literal; + hasMatchingReference(source); + } + }, + Program(node) { + if (lib === 'always' && path === 'always' && types == 'always') { + return; + } + programNode = node; + const referenceRegExp = /^\/\s* { + if (comment.type !== 'Line') { + return; + } + const referenceResult = referenceRegExp.exec(comment.value); + + if (referenceResult) { + if ( + (referenceResult[1] === 'types' && types === 'never') || + (referenceResult[1] === 'path' && path === 'never') || + (referenceResult[1] === 'lib' && lib === 'never') + ) { + context.report({ + node: comment, + messageId: 'tripleSlashReference', + data: { + module: referenceResult[2], + }, + }); + return; + } + if (referenceResult[1] === 'types' && types === 'prefer-import') { + references.push({ comment, importName: referenceResult[2] }); + } + } + }); + }, + }; + }, +}); diff --git a/packages/eslint-plugin/tests/rules/ban-types.test.ts b/packages/eslint-plugin/tests/rules/ban-types.test.ts index 12766b0438a0..73427fcc8064 100644 --- a/packages/eslint-plugin/tests/rules/ban-types.test.ts +++ b/packages/eslint-plugin/tests/rules/ban-types.test.ts @@ -16,6 +16,10 @@ const options: InferOptionsTypeFromRule = [ Object: "Use '{}' instead.", Array: null, F: null, + 'NS.Bad': { + message: 'Use NS.Good instead.', + fixWith: 'NS.Good', + }, }, }, ]; @@ -39,6 +43,14 @@ ruleTester.run('ban-types', rule, { code: 'let e: foo.String;', options, }, + { + code: 'let a: _.NS.Bad', + options, + }, + { + code: 'let a: NS.Bad._', + options, + }, ], invalid: [ { @@ -56,6 +68,25 @@ ruleTester.run('ban-types', rule, { ], options, }, + { + code: 'let aa: Foo;', + errors: [ + { + messageId: 'bannedTypeMessage', + data: { + name: 'Foo', + customMessage: '', + }, + }, + ], + options: [ + { + types: { + Foo: { message: '' }, + }, + }, + ], + }, { code: 'let b: {c: String};', output: 'let b: {c: string};', @@ -217,5 +248,52 @@ class Foo extends Bar implements Baz { ], options, }, + { + code: 'let a: NS.Bad;', + output: 'let a: NS.Good;', + errors: [ + { + messageId: 'bannedTypeMessage', + data: { + name: 'NS.Bad', + customMessage: ' Use NS.Good instead.', + }, + line: 1, + column: 8, + }, + ], + options, + }, + { + code: ` +let a: NS.Bad; +let b: Foo; + `, + output: ` +let a: NS.Good; +let b: Foo; + `, + errors: [ + { + messageId: 'bannedTypeMessage', + data: { + name: 'NS.Bad', + customMessage: ' Use NS.Good instead.', + }, + line: 2, + column: 8, + }, + { + messageId: 'bannedTypeMessage', + data: { + name: 'NS.Bad', + customMessage: ' Use NS.Good instead.', + }, + line: 3, + column: 12, + }, + ], + options, + }, ], }); diff --git a/packages/eslint-plugin/tests/rules/indent/indent.test.ts b/packages/eslint-plugin/tests/rules/indent/indent.test.ts index e992b440c6cc..9fb9ef7dadd6 100644 --- a/packages/eslint-plugin/tests/rules/indent/indent.test.ts +++ b/packages/eslint-plugin/tests/rules/indent/indent.test.ts @@ -768,6 +768,8 @@ const div: JQuery = $('
') `, options: [2, { VariableDeclarator: { const: 3 } }], }, + // https://github.com/typescript-eslint/typescript-eslint/issues/441 + `const;`, ], invalid: [ ...individualNodeTests.invalid, diff --git a/packages/eslint-plugin/tests/rules/no-explicit-any.test.ts b/packages/eslint-plugin/tests/rules/no-explicit-any.test.ts index cf596ffecbc3..b27cc8111bc0 100644 --- a/packages/eslint-plugin/tests/rules/no-explicit-any.test.ts +++ b/packages/eslint-plugin/tests/rules/no-explicit-any.test.ts @@ -1,5 +1,8 @@ -import rule from '../../src/rules/no-explicit-any'; +import rule, { MessageIds, Options } from '../../src/rules/no-explicit-any'; import { RuleTester } from '../RuleTester'; +import { TSESLint } from '@typescript-eslint/experimental-utils'; + +type InvalidTestCase = TSESLint.InvalidTestCase; const ruleTester = new RuleTester({ parser: '@typescript-eslint/parser', @@ -187,7 +190,7 @@ type obj = { options: [{ ignoreRestArgs: true }], }, ], - invalid: [ + invalid: ([ { code: 'const number: any = 1', errors: [ @@ -784,5 +787,23 @@ type obj = { }, ], }, - ], + ] as InvalidTestCase[]).reduce((acc, testCase) => { + acc.push(testCase); + const options = testCase.options || []; + const code = `// fixToUnknown: true\n${testCase.code}`; + acc.push({ + code, + output: code.replace(/any/g, 'unknown'), + options: [{ ...options[0], fixToUnknown: true }], + errors: testCase.errors.map(err => { + if (err.line === undefined) { + return err; + } + + return { ...err, line: err.line + 1 }; + }), + }); + + return acc; + }, []), }); diff --git a/packages/eslint-plugin/tests/rules/no-triple-slash-reference.test.ts b/packages/eslint-plugin/tests/rules/no-triple-slash-reference.test.ts index 5beaf104e52b..f120d5262432 100644 --- a/packages/eslint-plugin/tests/rules/no-triple-slash-reference.test.ts +++ b/packages/eslint-plugin/tests/rules/no-triple-slash-reference.test.ts @@ -22,7 +22,7 @@ let a code: '/// ', errors: [ { - messageId: 'tripleSlashReference', + messageId: 'noTripleSlashReference', line: 1, column: 1, }, @@ -36,7 +36,7 @@ let a parser: '@typescript-eslint/parser', errors: [ { - messageId: 'tripleSlashReference', + messageId: 'noTripleSlashReference', line: 2, column: 1, }, diff --git a/packages/eslint-plugin/tests/rules/strict-boolean-expressions.test.ts b/packages/eslint-plugin/tests/rules/strict-boolean-expressions.test.ts new file mode 100644 index 000000000000..030bade0f480 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/strict-boolean-expressions.test.ts @@ -0,0 +1,907 @@ +import path from 'path'; +import rule from '../../src/rules/strict-boolean-expressions'; +import { RuleTester } from '../RuleTester'; + +const rootPath = path.join(process.cwd(), 'tests/fixtures/'); + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', + parserOptions: { + tsconfigRootDir: rootPath, + project: './tsconfig.json', + }, +}); + +ruleTester.run('strict-boolean-expressions', rule, { + valid: [ + ` + let val = true; + let bool = !val; + let bool2 = true || val; + let bool3 = true && val; + `, + ` + let a = 0; + let u1 = typeof a; + let u2 = -a; + let u3 = ~a; + `, + ` + const bool1 = true; + const bool2 = false; + if (true) { + return; + } + + if (bool1) { + return; + } + + if (bool1 && bool2) { + return; + } + + if (bool1 || bool2) { + return; + } + + if ((bool1 && bool2) || (bool1 || bool2)) { + return; + } + `, + ` + const bool1 = true; + const bool2 = false; + const res1 = true ? true : false; + const res2 = bool1 && bool2 ? true : false; + const res3 = bool1 || bool2 ? true : false; + const res4 = (bool1 && bool2) || (bool1 || bool2) ? true : false; + `, + ` + for (let i = 0; true; i++) { + break; + } + `, + ` + const bool = true; + for (let i = 0; bool; i++) { + break; + } + `, + ` + const bool1 = true; + const bool2 = false; + for (let i = 0; bool1 && bool2; i++) { + break; + } + `, + ` + const bool1 = true; + const bool2 = false; + for (let i = 0; bool1 || bool2; i++) { + break; + } + `, + ` + const bool1 = true; + const bool2 = false; + for (let i = 0; (bool1 && bool2) || (bool1 || bool2); i++) { + break; + } + `, + ` + while (true) { + break; + } + `, + ` + const bool = true; + while (bool) { + break; + } + `, + ` + const bool1 = true; + const bool2 = false; + while (bool1 && bool2) { + break; + } + `, + ` + const bool1 = true; + const bool2 = false; + while (bool1 || bool2) { + break; + } + `, + ` + const bool1 = true; + const bool2 = false; + while ((bool1 && bool2) || (bool1 || bool2)) { + break; + } + `, + ` + do { + break; + } while (true); + `, + ` + const bool = true; + do { + break; + } while (bool); + `, + ` + const bool1 = true; + const bool2 = false; + do { + break; + } while (bool1 && bool2); + `, + ` + const bool1 = true; + const bool2 = false; + do { + break; + } while (bool1 || bool2); + `, + ` + const bool1 = true; + const bool2 = false; + do { + break; + } while ((bool1 && bool2) || (bool1 || bool2)); + `, + ` + function foo(arg: T) { return !arg; } + `, + ], + + invalid: [ + { + code: ` + let val = 1; + let bool = !val; + `, + errors: [ + { + messageId: 'strictBooleanExpression', + line: 3, + column: 21, + }, + ], + }, + { + code: ` + let val; + let bool = !val; + `, + errors: [ + { + messageId: 'strictBooleanExpression', + line: 3, + column: 21, + }, + ], + }, + { + code: ` + let val = 1; + let bool = true && val; + `, + errors: [ + { + messageId: 'strictBooleanExpression', + line: 3, + column: 20, + }, + ], + }, + { + code: ` + let val; + let bool = true && val; + `, + errors: [ + { + messageId: 'strictBooleanExpression', + line: 3, + column: 20, + }, + ], + }, + { + code: ` + let val = 1; + let bool = true || val; + `, + errors: [ + { + messageId: 'strictBooleanExpression', + line: 3, + column: 20, + }, + ], + }, + { + code: ` + let val; + let bool = true || val; + `, + errors: [ + { + messageId: 'strictBooleanExpression', + line: 3, + column: 20, + }, + ], + }, + { + code: ` + if (1) { + return; + } + `, + errors: [ + { + messageId: 'strictBooleanExpression', + line: 2, + column: 13, + }, + ], + }, + { + code: ` + if (undefined) { + return; + } + `, + errors: [ + { + messageId: 'strictBooleanExpression', + line: 2, + column: 13, + }, + ], + }, + { + code: ` + let item = 1; + if (item) { + return; + } + `, + errors: [ + { + messageId: 'strictBooleanExpression', + line: 3, + column: 13, + }, + ], + }, + { + code: ` + let item; + if (item) { + return; + } + `, + errors: [ + { + messageId: 'strictBooleanExpression', + line: 3, + column: 13, + }, + ], + }, + { + code: ` + let item1 = true; + let item2 = 1; + if (item1 && item2) { + return; + } + `, + errors: [ + { + messageId: 'strictBooleanExpression', + line: 4, + column: 13, + }, + ], + }, + { + code: ` + let item1 = 1; + let item2 = true; + if (item1 && item2) { + return; + } + `, + errors: [ + { + messageId: 'strictBooleanExpression', + line: 4, + column: 13, + }, + ], + }, + { + code: ` + let item1; + let item2 = true; + if (item1 && item2) { + return; + } + `, + errors: [ + { + messageId: 'strictBooleanExpression', + line: 4, + column: 13, + }, + ], + }, + { + code: ` + let item1 = true; + let item2 = 1; + if (item1 || item2) { + return; + } + `, + errors: [ + { + messageId: 'strictBooleanExpression', + line: 4, + column: 13, + }, + ], + }, + { + code: ` + let item1 = 1; + let item2 = true; + if (item1 || item2) { + return; + } + `, + errors: [ + { + messageId: 'strictBooleanExpression', + line: 4, + column: 13, + }, + ], + }, + { + code: ` + let item1; + let item2 = true; + if (item1 || item2) { + return; + } + `, + errors: [ + { + messageId: 'strictBooleanExpression', + line: 4, + column: 13, + }, + ], + }, + { + code: ` + const bool = 1 ? true : false; + `, + errors: [ + { + messageId: 'strictBooleanExpression', + line: 2, + column: 22, + }, + ], + }, + { + code: ` + const bool = undefined ? true : false; + `, + errors: [ + { + messageId: 'strictBooleanExpression', + line: 2, + column: 22, + }, + ], + }, + { + code: ` + let item = 1; + const bool = item ? true : false; + `, + errors: [ + { + messageId: 'strictBooleanExpression', + line: 3, + column: 22, + }, + ], + }, + { + code: ` + let item; + const bool = item ? true : false; + `, + errors: [ + { + messageId: 'strictBooleanExpression', + line: 3, + column: 22, + }, + ], + }, + { + code: ` + let item1 = 1; + let item2 = false; + const bool = item1 && item2 ? true : false; + `, + errors: [ + { + messageId: 'strictBooleanExpression', + line: 4, + column: 22, + }, + ], + }, + { + code: ` + let item1 = true; + let item2 = 1; + const bool = item1 && item2 ? true : false; + `, + errors: [ + { + messageId: 'strictBooleanExpression', + line: 4, + column: 22, + }, + ], + }, + { + code: ` + let item1 = true; + let item2; + const bool = item1 && item2 ? true : false; + `, + errors: [ + { + messageId: 'strictBooleanExpression', + line: 4, + column: 22, + }, + ], + }, + { + code: ` + let item1 = 1; + let item2 = false; + const bool = item1 || item2 ? true : false; + `, + errors: [ + { + messageId: 'strictBooleanExpression', + line: 4, + column: 22, + }, + ], + }, + { + code: ` + let item1 = true; + let item2 = 1; + const bool = item1 || item2 ? true : false; + `, + errors: [ + { + messageId: 'strictBooleanExpression', + line: 4, + column: 22, + }, + ], + }, + { + code: ` + let item1 = true; + let item2; + const bool = item1 || item2 ? true : false; + `, + errors: [ + { + messageId: 'strictBooleanExpression', + line: 4, + column: 22, + }, + ], + }, + { + code: ` + for (let i = 0; 1; i++) { + return; + } + `, + errors: [ + { + messageId: 'strictBooleanExpression', + line: 2, + column: 25, + }, + ], + }, + { + code: ` + for (let i = 0; undefined; i++) { + return; + } + `, + errors: [ + { + messageId: 'strictBooleanExpression', + line: 2, + column: 25, + }, + ], + }, + { + code: ` + let bool = 1; + for (let i = 0; bool; i++) { + return; + } + `, + errors: [ + { + messageId: 'strictBooleanExpression', + line: 3, + column: 25, + }, + ], + }, + { + code: ` + let bool; + for (let i = 0; bool; i++) { + return; + } + `, + errors: [ + { + messageId: 'strictBooleanExpression', + line: 3, + column: 25, + }, + ], + }, + { + code: ` + let bool1 = 1; + let bool2 = true; + for (let i = 0; bool1 && bool2; i++) { + return; + } + `, + errors: [ + { + messageId: 'strictBooleanExpression', + line: 4, + column: 25, + }, + ], + }, + { + code: ` + let bool1; + let bool2 = true; + for (let i = 0; bool1 && bool2; i++) { + return; + } + `, + errors: [ + { + messageId: 'strictBooleanExpression', + line: 4, + column: 25, + }, + ], + }, + { + code: ` + let bool1 = 1; + let bool2 = true; + for (let i = 0; bool1 || bool2; i++) { + return; + } + `, + errors: [ + { + messageId: 'strictBooleanExpression', + line: 4, + column: 25, + }, + ], + }, + { + code: ` + let bool1; + let bool2 = true; + for (let i = 0; bool1 || bool2; i++) { + return; + } + `, + errors: [ + { + messageId: 'strictBooleanExpression', + line: 4, + column: 25, + }, + ], + }, + { + code: ` + while (1) { + return; + } + `, + errors: [ + { + messageId: 'strictBooleanExpression', + line: 2, + column: 16, + }, + ], + }, + { + code: ` + while (undefined) { + return; + } + `, + errors: [ + { + messageId: 'strictBooleanExpression', + line: 2, + column: 16, + }, + ], + }, + { + code: ` + let bool = 1; + while (bool) { + return; + } + `, + errors: [ + { + messageId: 'strictBooleanExpression', + line: 3, + column: 16, + }, + ], + }, + { + code: ` + let bool; + while (bool) { + return; + } + `, + errors: [ + { + messageId: 'strictBooleanExpression', + line: 3, + column: 16, + }, + ], + }, + { + code: ` + let bool1 = 1; + let bool2 = true; + while (bool1 && bool2) { + return; + } + `, + errors: [ + { + messageId: 'strictBooleanExpression', + line: 4, + column: 16, + }, + ], + }, + { + code: ` + let bool1; + let bool2 = true; + while (bool1 && bool2) { + return; + } + `, + errors: [ + { + messageId: 'strictBooleanExpression', + line: 4, + column: 16, + }, + ], + }, + { + code: ` + let bool1 = 1; + let bool2 = true; + while (bool1 || bool2) { + return; + } + `, + errors: [ + { + messageId: 'strictBooleanExpression', + line: 4, + column: 16, + }, + ], + }, + { + code: ` + let bool1; + let bool2 = true; + while (bool1 || bool2) { + return; + } + `, + errors: [ + { + messageId: 'strictBooleanExpression', + line: 4, + column: 16, + }, + ], + }, + { + code: ` + do { + return; + } while (1); + `, + errors: [ + { + messageId: 'strictBooleanExpression', + line: 4, + column: 18, + }, + ], + }, + { + code: ` + do { + return; + } while (undefined); + `, + errors: [ + { + messageId: 'strictBooleanExpression', + line: 4, + column: 18, + }, + ], + }, + { + code: ` + let bool = 1; + do { + return; + } while (bool); + `, + errors: [ + { + messageId: 'strictBooleanExpression', + line: 5, + column: 18, + }, + ], + }, + { + code: ` + let bool; + do { + return; + } while (bool); + `, + errors: [ + { + messageId: 'strictBooleanExpression', + line: 5, + column: 18, + }, + ], + }, + { + code: ` + let bool1 = 1; + let bool2 = true; + do { + return; + } while (bool1 && bool2); + `, + errors: [ + { + messageId: 'strictBooleanExpression', + line: 6, + column: 18, + }, + ], + }, + { + code: ` + let bool1; + let bool2 = true; + do { + return; + } while (bool1 && bool2); + `, + errors: [ + { + messageId: 'strictBooleanExpression', + line: 6, + column: 18, + }, + ], + }, + { + code: ` + let bool1 = 1; + let bool2 = true; + do { + return; + } while (bool1 || bool2); + `, + errors: [ + { + messageId: 'strictBooleanExpression', + line: 6, + column: 18, + }, + ], + }, + { + code: ` + let bool1; + let bool2 = true; + do { + return; + } while (bool1 || bool2); + `, + errors: [ + { + messageId: 'strictBooleanExpression', + line: 6, + column: 18, + }, + ], + }, + { + code: ` + function foo(arg: T) { return !arg; } + `, + errors: [ + { + messageId: 'strictBooleanExpression', + line: 2, + column: 58, + }, + ], + }, + ], +}); diff --git a/packages/eslint-plugin/tests/rules/triple-slash-reference.test.ts b/packages/eslint-plugin/tests/rules/triple-slash-reference.test.ts new file mode 100644 index 000000000000..0785bf7a02ee --- /dev/null +++ b/packages/eslint-plugin/tests/rules/triple-slash-reference.test.ts @@ -0,0 +1,128 @@ +import rule from '../../src/rules/triple-slash-reference'; +import { RuleTester } from '../RuleTester'; + +const ruleTester = new RuleTester({ + parserOptions: { + sourceType: 'module', + }, + parser: '@typescript-eslint/parser', +}); + +ruleTester.run('triple-slash-reference', rule, { + valid: [ + { + code: ` + /// + /// + /// + import * as foo from "foo" + import * as bar from "bar" + import * as baz from "baz" + `, + options: [{ path: 'always', types: 'always', lib: 'always' }], + }, + { + code: ` + import * as foo from "foo" + `, + options: [{ path: 'never' }], + }, + { + code: ` + import * as foo from "foo" + `, + options: [{ types: 'never' }], + }, + { + code: ` + import * as foo from "foo" + `, + options: [{ lib: 'never' }], + }, + { + code: ` + import * as foo from "foo" + `, + options: [{ types: 'prefer-import' }], + }, + { + code: ` + /// + import * as bar from "bar" + `, + options: [{ types: 'prefer-import' }], + }, + { + code: ` + /* + /// + */ + import * as foo from "foo" + `, + options: [{ path: 'never', types: 'never', lib: 'never' }], + }, + ], + invalid: [ + { + code: ` +/// +import * as foo from "foo" + `, + options: [{ types: 'prefer-import' }], + errors: [ + { + messageId: 'tripleSlashReference', + line: 2, + column: 1, + }, + ], + }, + { + code: ` +/// +import foo = require("foo"); + `, + options: [{ types: 'prefer-import' }], + errors: [ + { + messageId: 'tripleSlashReference', + line: 2, + column: 1, + }, + ], + }, + { + code: `/// `, + options: [{ path: 'never' }], + errors: [ + { + messageId: 'tripleSlashReference', + line: 1, + column: 1, + }, + ], + }, + { + code: `/// `, + options: [{ types: 'never' }], + errors: [ + { + messageId: 'tripleSlashReference', + line: 1, + column: 1, + }, + ], + }, + { + code: `/// `, + options: [{ lib: 'never' }], + errors: [ + { + messageId: 'tripleSlashReference', + line: 1, + column: 1, + }, + ], + }, + ], +}); diff --git a/packages/typescript-estree/src/ts-estree/ts-estree.ts b/packages/typescript-estree/src/ts-estree/ts-estree.ts index e786e83e0dfb..8eed48390d0f 100644 --- a/packages/typescript-estree/src/ts-estree/ts-estree.ts +++ b/packages/typescript-estree/src/ts-estree/ts-estree.ts @@ -1387,6 +1387,7 @@ export interface UnaryExpression extends UnaryExpressionBase { export interface VariableDeclaration extends BaseNode { type: AST_NODE_TYPES.VariableDeclaration; + // NOTE - this is not guaranteed to have any elements in it. i.e. `const;` declarations: VariableDeclarator[]; kind: 'let' | 'const' | 'var'; declare?: boolean;