diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index 6d874a9c2b1c..b616245454e6 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/await-thenable`](./docs/rules/await-thenable.md) | Disallows awaiting a value that is not a Thenable | :heavy_check_mark: | | :thought_balloon: | | [`@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/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: | | | @@ -181,6 +181,7 @@ Then you should add `airbnb` (or `airbnb-base`) to your `extends` section of `.e | [`@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-arguments`](./docs/rules/no-unnecessary-type-arguments.md) | Warns if an explicitly specified type argument is the default for that type parameter | | :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 | :heavy_check_mark: | :wrench: | :thought_balloon: | +| [`@typescript-eslint/no-unused-expressions`](./docs/rules/no-unused-expressions.md) | Disallow unused expressions | | | | | [`@typescript-eslint/no-unused-vars`](./docs/rules/no-unused-vars.md) | Disallow unused variables | :heavy_check_mark: | | | | [`@typescript-eslint/no-use-before-define`](./docs/rules/no-use-before-define.md) | Disallow the use of variables before they are defined | :heavy_check_mark: | | | | [`@typescript-eslint/no-useless-constructor`](./docs/rules/no-useless-constructor.md) | Disallow unnecessary constructors | | | | @@ -193,7 +194,7 @@ Then you should add `airbnb` (or `airbnb-base`) to your `extends` section of `.e | [`@typescript-eslint/prefer-regexp-exec`](./docs/rules/prefer-regexp-exec.md) | Prefer RegExp#exec() over String#match() if no global flag is provided | :heavy_check_mark: | | :thought_balloon: | | [`@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/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-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: | diff --git a/packages/eslint-plugin/docs/rules/no-unused-expressions.md b/packages/eslint-plugin/docs/rules/no-unused-expressions.md new file mode 100644 index 000000000000..7da998ab2c6c --- /dev/null +++ b/packages/eslint-plugin/docs/rules/no-unused-expressions.md @@ -0,0 +1,25 @@ +# require or disallow semicolons instead of ASI (semi) + +This rule aims to eliminate unused expressions which have no effect on the state of the program. + +## Rule Details + +This rule extends the base [eslint/no-unused-expressions](https://eslint.org/docs/rules/no-unused-expressions) rule. +It supports all options and features of the base rule. +This version adds support for numerous typescript features. + +## How to use + +```cjson +{ + // note you must disable the base rule as it can report incorrect errors + "no-unused-expressions": "off", + "@typescript-eslint/no-unused-expressions": ["error"] +} +``` + +## Options + +See [eslint/no-unused-expressions options](https://eslint.org/docs/rules/no-unused-expressions#options). + +Taken with ❤️ [from ESLint core](https://github.com/eslint/eslint/blob/master/docs/rules/no-unused-expressions.md) diff --git a/packages/eslint-plugin/src/configs/all.json b/packages/eslint-plugin/src/configs/all.json index 377f4b58f0e4..395c1af592ed 100644 --- a/packages/eslint-plugin/src/configs/all.json +++ b/packages/eslint-plugin/src/configs/all.json @@ -50,6 +50,7 @@ "@typescript-eslint/no-unnecessary-qualifier": "error", "@typescript-eslint/no-unnecessary-type-arguments": "error", "@typescript-eslint/no-unnecessary-type-assertion": "error", + "@typescript-eslint/no-unused-expressions": "error", "no-unused-vars": "off", "@typescript-eslint/no-unused-vars": "error", "no-use-before-define": "off", diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index 5302abd05de0..4aa8beea1f63 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -39,6 +39,7 @@ import noUnnecessaryCondition from './no-unnecessary-condition'; import noUnnecessaryQualifier from './no-unnecessary-qualifier'; import noUnnecessaryTypeAssertion from './no-unnecessary-type-assertion'; import noUnusedVars from './no-unused-vars'; +import noUnusedExpressions from './no-unused-expressions'; import noUseBeforeDefine from './no-use-before-define'; import noUselessConstructor from './no-useless-constructor'; import noVarRequires from './no-var-requires'; @@ -106,6 +107,7 @@ export default { 'no-unnecessary-type-arguments': useDefaultTypeParameter, 'no-unnecessary-type-assertion': noUnnecessaryTypeAssertion, 'no-unused-vars': noUnusedVars, + 'no-unused-expressions': noUnusedExpressions, 'no-use-before-define': noUseBeforeDefine, 'no-useless-constructor': noUselessConstructor, 'no-var-requires': noVarRequires, diff --git a/packages/eslint-plugin/src/rules/no-unused-expressions.ts b/packages/eslint-plugin/src/rules/no-unused-expressions.ts new file mode 100644 index 000000000000..f7d86f50c1c7 --- /dev/null +++ b/packages/eslint-plugin/src/rules/no-unused-expressions.ts @@ -0,0 +1,33 @@ +import { AST_NODE_TYPES } from '@typescript-eslint/experimental-utils'; +import baseRule from 'eslint/lib/rules/no-unused-expressions'; +import * as util from '../util'; + +export default util.createRule({ + name: 'no-unused-expressions', + meta: { + type: 'suggestion', + docs: { + description: 'Disallow unused expressions', + category: 'Best Practices', + recommended: false, + }, + schema: baseRule.meta.schema, + messages: { + expected: + 'Expected an assignment or function call and instead saw an expression.', + }, + }, + defaultOptions: [], + create(context) { + const rules = baseRule.create(context); + + return { + ExpressionStatement(node): void { + if (node.expression.type === AST_NODE_TYPES.OptionalCallExpression) { + return; + } + rules.ExpressionStatement(node); + }, + }; + }, +}); diff --git a/packages/eslint-plugin/tests/rules/no-unused-expressions.test.ts b/packages/eslint-plugin/tests/rules/no-unused-expressions.test.ts new file mode 100644 index 000000000000..46653b30c663 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/no-unused-expressions.test.ts @@ -0,0 +1,180 @@ +import rule from '../../src/rules/no-unused-expressions'; +import { RuleTester } from '../RuleTester'; + +const ruleTester = new RuleTester({ + parserOptions: { + ecmaVersion: 6, + sourceType: 'module', + ecmaFeatures: {}, + }, + parser: '@typescript-eslint/parser', +}); + +// the base rule doesn't have messageIds +function error( + messages: { line: number; column: number }[], + // eslint-disable-next-line @typescript-eslint/no-explicit-any +): any[] { + return messages.map(message => ({ + ...message, + message: + 'Expected an assignment or function call and instead saw an expression.', + })); +} + +ruleTester.run('no-unused-expressions', rule, { + valid: [ + ` + test.age?.toLocaleString(); + `, + ` + let a = (a?.b).c; + `, + ` + let b = a?.['b']; + `, + ` + let c = one[2]?.[3][4]; + `, + ` + one[2]?.[3][4]?.(); + `, + ` + a?.['b']?.c(); + `, + ], + invalid: [ + { + code: ` +if(0) 0 + `, + errors: error([ + { + line: 2, + column: 7, + }, + ]), + }, + { + code: ` +f(0), {} + `, + errors: error([ + { + line: 2, + column: 1, + }, + ]), + }, + { + code: ` +a, b() + `, + errors: error([ + { + line: 2, + column: 1, + }, + ]), + }, + { + code: ` +a() && function namedFunctionInExpressionContext () {f();} + `, + errors: error([ + { + line: 2, + column: 1, + }, + ]), + }, + { + code: ` +a?.b + `, + errors: error([ + { + line: 2, + column: 1, + }, + ]), + }, + { + code: ` +(a?.b).c + `, + errors: error([ + { + line: 2, + column: 1, + }, + ]), + }, + { + code: ` +a?.['b'] + `, + errors: error([ + { + line: 2, + column: 1, + }, + ]), + }, + { + code: ` +(a?.['b']).c + `, + errors: error([ + { + line: 2, + column: 1, + }, + ]), + }, + { + code: ` +a?.b()?.c + `, + errors: error([ + { + line: 2, + column: 1, + }, + ]), + }, + { + code: ` +(a?.b()).c + `, + errors: error([ + { + line: 2, + column: 1, + }, + ]), + }, + { + code: ` +one[2]?.[3][4]; + `, + errors: error([ + { + line: 2, + column: 1, + }, + ]), + }, + { + code: ` +one.two?.three.four; + `, + errors: error([ + { + line: 2, + column: 1, + }, + ]), + }, + ], +}); diff --git a/packages/eslint-plugin/typings/eslint-rules.d.ts b/packages/eslint-plugin/typings/eslint-rules.d.ts index 120b11433cb9..c21b235be448 100644 --- a/packages/eslint-plugin/typings/eslint-rules.d.ts +++ b/packages/eslint-plugin/typings/eslint-rules.d.ts @@ -304,6 +304,26 @@ declare module 'eslint/lib/rules/no-unused-vars' { export = rule; } +declare module 'eslint/lib/rules/no-unused-expressions' { + import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils'; + + const rule: TSESLint.RuleModule< + 'expected', + ( + | 'all' + | 'local' + | { + allowShortCircuit?: boolean; + allowTernary?: boolean; + allowTaggedTemplates?: boolean; + })[], + { + ExpressionStatement(node: TSESTree.ExpressionStatement): void; + } + >; + export = rule; +} + declare module 'eslint/lib/rules/no-use-before-define' { import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils';