Skip to content

Commit

Permalink
Merge branch 'master' into feat-init-declarations
Browse files Browse the repository at this point in the history
  • Loading branch information
bradzacher committed Apr 20, 2020
2 parents 53c4b3d + cea51bf commit 69086c8
Show file tree
Hide file tree
Showing 11 changed files with 529 additions and 6 deletions.
1 change: 1 addition & 0 deletions packages/eslint-plugin/README.md
Expand Up @@ -184,6 +184,7 @@ In these cases, we create what we call an extension rule; a rule within our plug
| [`@typescript-eslint/func-call-spacing`](./docs/rules/func-call-spacing.md) | Require or disallow spacing between function identifiers and their invocations | | :wrench: | |
| [`@typescript-eslint/indent`](./docs/rules/indent.md) | Enforce consistent indentation | | :wrench: | |
| [`@typescript-eslint/init-declarations`](./docs/rules/init-declarations.md) | require or disallow initialization in variable declarations | | | |
| [`@typescript-eslint/keyword-spacing`](./docs/rules/keyword-spacing.md) | Enforce consistent spacing before and after keywords | | :wrench: | |
| [`@typescript-eslint/no-array-constructor`](./docs/rules/no-array-constructor.md) | Disallow generic `Array` constructors | :heavy_check_mark: | :wrench: | |
| [`@typescript-eslint/no-dupe-class-members`](./docs/rules/no-dupe-class-members.md) | Disallow duplicate class members | | | |
| [`@typescript-eslint/no-empty-function`](./docs/rules/no-empty-function.md) | Disallow empty functions | :heavy_check_mark: | | |
Expand Down
22 changes: 22 additions & 0 deletions packages/eslint-plugin/docs/rules/keyword-spacing.md
@@ -0,0 +1,22 @@
# Enforce consistent spacing before and after keywords (`keyword-spacing`)

## Rule Details

This rule extends the base [`eslint/keyword-spacing`](https://eslint.org/docs/rules/keyword-spacing) rule.
This version adds support for generic type parameters on function calls.

## How to use

```cjson
{
// note you must disable the base rule as it can report incorrect errors
"keyword-spacing": "off",
"@typescript-eslint/keyword-spacing": ["error"]
}
```

## Options

See [`eslint/keyword-spacing` options](https://eslint.org/docs/rules/keyword-spacing#options).

<sup>Taken with ❤️ [from ESLint core](https://github.com/eslint/eslint/blob/master/docs/rules/keyword-spacing.md)</sup>
24 changes: 21 additions & 3 deletions packages/eslint-plugin/docs/rules/no-floating-promises.md
Expand Up @@ -51,6 +51,8 @@ The rule accepts an options object with the following properties:
type Options = {
// if true, checking void expressions will be skipped
ignoreVoid?: boolean;
// if true, checking for async iife will be skipped
ignoreIIFE?: boolean;
};

const defaults = {
Expand All @@ -60,7 +62,8 @@ const defaults = {

### `ignoreVoid`

This allows to easily suppress false-positives with void operator.
This allows you to stop the rule reporting promises consumed with void operator.
This can be a good way to explicitly mark a promise as intentionally not awaited.

Examples of **correct** code for this rule with `{ ignoreVoid: true }`:

Expand All @@ -73,10 +76,25 @@ void returnsPromise();
void Promise.reject('value');
```

### `ignoreIIFE`

This allows you to skip checking of async iife

Examples of **correct** code for this rule with `{ ignoreIIFE: true }`:

```ts
await(async function() {
await res(1);
})();

(async function() {
await res(1);
})();
```

## When Not To Use It

If you do not use Promise-like values in your codebase or want to allow them to
remain unhandled.
If you do not use Promise-like values in your codebase, or want to allow them to remain unhandled.

## Related to

Expand Down
2 changes: 2 additions & 0 deletions packages/eslint-plugin/src/configs/all.json
Expand Up @@ -24,6 +24,8 @@
"@typescript-eslint/indent": "error",
"init-declarations": "off",
"@typescript-eslint/init-declarations": "error",
"keyword-spacing": "off",
"@typescript-eslint/keyword-spacing": "error",
"@typescript-eslint/member-delimiter-style": "error",
"@typescript-eslint/member-ordering": "error",
"@typescript-eslint/method-signature-style": "error",
Expand Down
4 changes: 3 additions & 1 deletion packages/eslint-plugin/src/rules/index.ts
Expand Up @@ -19,6 +19,7 @@ import funcCallSpacing from './func-call-spacing';
import genericTypeNaming from './generic-type-naming';
import indent from './indent';
import interfaceNamePrefix from './interface-name-prefix';
import keywordSpacing from './keyword-spacing';
import memberDelimiterStyle from './member-delimiter-style';
import memberNaming from './member-naming';
import memberOrdering from './member-ordering';
Expand All @@ -31,10 +32,10 @@ import noDynamicDelete from './no-dynamic-delete';
import noEmptyFunction from './no-empty-function';
import noEmptyInterface from './no-empty-interface';
import noExplicitAny from './no-explicit-any';
import noExtraneousClass from './no-extraneous-class';
import noExtraNonNullAssertion from './no-extra-non-null-assertion';
import noExtraParens from './no-extra-parens';
import noExtraSemi from './no-extra-semi';
import noExtraneousClass from './no-extraneous-class';
import noFloatingPromises from './no-floating-promises';
import noForInArray from './no-for-in-array';
import noImpliedEval from './no-implied-eval';
Expand Down Expand Up @@ -120,6 +121,7 @@ export default {
indent: indent,
'init-declarations': initDeclarations,
'interface-name-prefix': interfaceNamePrefix,
'keyword-spacing': keywordSpacing,
'member-delimiter-style': memberDelimiterStyle,
'member-naming': memberNaming,
'member-ordering': memberOrdering,
Expand Down
52 changes: 52 additions & 0 deletions packages/eslint-plugin/src/rules/keyword-spacing.ts
@@ -0,0 +1,52 @@
import { AST_TOKEN_TYPES } from '@typescript-eslint/experimental-utils';
import baseRule from 'eslint/lib/rules/keyword-spacing';
import * as util from '../util';

export type Options = util.InferOptionsTypeFromRule<typeof baseRule>;
export type MessageIds = util.InferMessageIdsTypeFromRule<typeof baseRule>;

export default util.createRule<Options, MessageIds>({
name: 'keyword-spacing',
meta: {
type: 'layout',
docs: {
description: 'Enforce consistent spacing before and after keywords',
category: 'Stylistic Issues',
recommended: false,
extendsBaseRule: true,
},
fixable: 'whitespace',
schema: baseRule.meta.schema,
messages: baseRule.meta.messages,
},
defaultOptions: [{}],

create(context) {
const sourceCode = context.getSourceCode();
const baseRules = baseRule.create(context);
return {
...baseRules,
TSAsExpression(node): void {
const asToken = util.nullThrows(
sourceCode.getTokenAfter(
node.expression,
token => token.value === 'as',
),
util.NullThrowsReasons.MissingToken('as', node.type),
);
const oldTokenType = asToken.type;
// as is a contextual keyword, so it's always reported as an Identifier
// the rule looks for keyword tokens, so we temporarily override it
// we mutate it at the token level because the rule calls sourceCode.getFirstToken,
// so mutating a copy would not change the underlying copy returned by that method
asToken.type = AST_TOKEN_TYPES.Keyword;

// use this selector just because it is just a call to `checkSpacingAroundFirstToken`
baseRules.DebuggerStatement(asToken as never);

// make sure to reset the type afterward so we don't permanently mutate the AST
asToken.type = oldTokenType;
},
};
},
});
26 changes: 25 additions & 1 deletion packages/eslint-plugin/src/rules/no-floating-promises.ts
@@ -1,12 +1,17 @@
import * as tsutils from 'tsutils';
import * as ts from 'typescript';
import { TSESLint } from '@typescript-eslint/experimental-utils';
import {
TSESLint,
AST_NODE_TYPES,
TSESTree,
} from '@typescript-eslint/experimental-utils';

import * as util from '../util';

type Options = [
{
ignoreVoid?: boolean;
ignoreIIFE?: boolean;
},
];

Expand All @@ -33,6 +38,7 @@ export default util.createRule<Options, MessageId>({
type: 'object',
properties: {
ignoreVoid: { type: 'boolean' },
ignoreIIFE: { type: 'boolean' },
},
additionalProperties: false,
},
Expand All @@ -42,6 +48,7 @@ export default util.createRule<Options, MessageId>({
defaultOptions: [
{
ignoreVoid: false,
ignoreIIFE: false,
},
],

Expand All @@ -54,6 +61,10 @@ export default util.createRule<Options, MessageId>({
ExpressionStatement(node): void {
const { expression } = parserServices.esTreeNodeToTSNodeMap.get(node);

if (options.ignoreIIFE && isAsyncIife(node)) {
return;
}

if (isUnhandledPromise(checker, expression)) {
if (options.ignoreVoid) {
context.report({
Expand All @@ -80,6 +91,19 @@ export default util.createRule<Options, MessageId>({
},
};

function isAsyncIife(node: TSESTree.ExpressionStatement): boolean {
if (node.expression.type !== AST_NODE_TYPES.CallExpression) {
return false;
}

return (
node.expression.type === AST_NODE_TYPES.CallExpression &&
(node.expression.callee.type ===
AST_NODE_TYPES.ArrowFunctionExpression ||
node.expression.callee.type === AST_NODE_TYPES.FunctionExpression)
);
}

function isUnhandledPromise(
checker: ts.TypeChecker,
node: ts.Node,
Expand Down
153 changes: 153 additions & 0 deletions packages/eslint-plugin/tests/rules/keyword-spacing.test.ts
@@ -0,0 +1,153 @@
/* eslint-disable eslint-comments/no-use */
// this rule tests the spacing, which prettier will want to fix and break the tests
/* eslint "@typescript-eslint/internal/plugin-test-formatting": ["error", { formatWithPrettier: false }] */
/* eslint-enable eslint-comments/no-use */
import { TSESLint } from '@typescript-eslint/experimental-utils';
import rule, { MessageIds, Options } from '../../src/rules/keyword-spacing';
import { RuleTester } from '../RuleTester';

//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------

const BOTH = { before: true, after: true };
const NEITHER = { before: false, after: false };

/**
* Creates an option object to test an 'overrides' option.
*
* e.g.
*
* override('as', BOTH)
*
* returns
*
* {
* before: false,
* after: false,
* overrides: {as: {before: true, after: true}}
* }
* @param keyword A keyword to be overridden.
* @param value A value to override.
* @returns An option object to test an 'overrides' option.
*/
function overrides(keyword: string, value: Options[0]): Options[0] {
return {
before: value.before === false,
after: value.after === false,
overrides: { [keyword]: value },
};
}

/**
* Gets an error message that expected space(s) before a specified keyword.
* @param keyword A keyword.
* @returns An error message.
*/
function expectedBefore(keyword: string): TSESLint.TestCaseError<MessageIds>[] {
return [{ messageId: 'expectedBefore', data: { value: keyword } }];
}

/**
* Gets an error message that expected space(s) after a specified keyword.
* @param keyword A keyword.
* @returns An error message.
*/
function expectedAfter(keyword: string): TSESLint.TestCaseError<MessageIds>[] {
return [{ messageId: 'expectedAfter', data: { value: keyword } }];
}

/**
* Gets an error message that unexpected space(s) before a specified keyword.
* @param keyword A keyword.
* @returns An error message.
*/
function unexpectedBefore(
keyword: string,
): TSESLint.TestCaseError<MessageIds>[] {
return [{ messageId: 'unexpectedBefore', data: { value: keyword } }];
}

/**
* Gets an error message that unexpected space(s) after a specified keyword.
* @param keyword A keyword.
* @returns An error message.
*/
function unexpectedAfter(
keyword: string,
): TSESLint.TestCaseError<MessageIds>[] {
return [{ messageId: 'unexpectedAfter', data: { value: keyword } }];
}

const ruleTester = new RuleTester({
parser: '@typescript-eslint/parser',
});

ruleTester.run('keyword-spacing', rule, {
valid: [
//----------------------------------------------------------------------
// as (typing)
//----------------------------------------------------------------------
{
code: 'const foo = {} as {};',
parserOptions: { ecmaVersion: 6, sourceType: 'module' },
},
{
code: 'const foo = {}as{};',
options: [NEITHER],
parserOptions: { ecmaVersion: 6, sourceType: 'module' },
},
{
code: 'const foo = {} as {};',
options: [overrides('as', BOTH)],
parserOptions: { ecmaVersion: 6, sourceType: 'module' },
},
{
code: 'const foo = {}as{};',
options: [overrides('as', NEITHER)],
parserOptions: { ecmaVersion: 6, sourceType: 'module' },
},
{
code: 'const foo = {} as {};',
options: [{ overrides: { as: {} } }],
parserOptions: { ecmaVersion: 6, sourceType: 'module' },
},
],
invalid: [
//----------------------------------------------------------------------
// as (typing)
//----------------------------------------------------------------------
{
code: 'const foo = {}as {};',
output: 'const foo = {} as {};',
parserOptions: { ecmaVersion: 6, sourceType: 'module' },
errors: expectedBefore('as'),
},
{
code: 'const foo = {} as{};',
output: 'const foo = {}as{};',
options: [NEITHER],
parserOptions: { ecmaVersion: 6, sourceType: 'module' },
errors: unexpectedBefore('as'),
},
{
code: 'const foo = {} as{};',
output: 'const foo = {} as {};',
parserOptions: { ecmaVersion: 6, sourceType: 'module' },
errors: expectedAfter('as'),
},
{
code: 'const foo = {}as {};',
output: 'const foo = {}as{};',
options: [NEITHER],
parserOptions: { ecmaVersion: 6, sourceType: 'module' },
errors: unexpectedAfter('as'),
},
{
code: 'const foo = {} as{};',
options: [{ overrides: { as: {} } }],
parserOptions: { ecmaVersion: 6, sourceType: 'module' },
errors: expectedAfter('as'),
},
],
});

0 comments on commit 69086c8

Please sign in to comment.