Skip to content

Commit

Permalink
feat(eslint-plugin): [no-unused-expressions] extend for optional chai…
Browse files Browse the repository at this point in the history
…ning (#1175)
  • Loading branch information
Nizarius authored and bradzacher committed Nov 11, 2019
1 parent 026ceb9 commit 57d63b7
Show file tree
Hide file tree
Showing 7 changed files with 264 additions and 2 deletions.
5 changes: 3 additions & 2 deletions packages/eslint-plugin/README.md
Expand Up @@ -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: | | |
Expand Down Expand Up @@ -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 | | | |
Expand All @@ -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: |
Expand Down
25 changes: 25 additions & 0 deletions 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).

<sup>Taken with ❤️ [from ESLint core](https://github.com/eslint/eslint/blob/master/docs/rules/no-unused-expressions.md)</sup>
1 change: 1 addition & 0 deletions packages/eslint-plugin/src/configs/all.json
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions packages/eslint-plugin/src/rules/index.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand Down
33 changes: 33 additions & 0 deletions 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);
},
};
},
});
180 changes: 180 additions & 0 deletions 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,
},
]),
},
],
});
20 changes: 20 additions & 0 deletions packages/eslint-plugin/typings/eslint-rules.d.ts
Expand Up @@ -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';

Expand Down

0 comments on commit 57d63b7

Please sign in to comment.