Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(eslint-plugin): add extension rule keyword-spacing #1739

Merged
merged 32 commits into from Apr 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
16b5b1b
keyword-spacing
OoDeLally Mar 16, 2020
fca75e7
Cleaned
OoDeLally Mar 16, 2020
82f1109
Doc
OoDeLally Mar 16, 2020
47b6f2d
all.json
OoDeLally Mar 16, 2020
668e9e9
Docs
OoDeLally Mar 16, 2020
936913b
Prettified
OoDeLally Mar 16, 2020
e0dc659
removed as any
OoDeLally Mar 16, 2020
644cb33
Import what's available from base rule
OoDeLally Mar 16, 2020
23975f2
Moved keyword typing file
OoDeLally Mar 16, 2020
f0e95bf
Prettyfied
OoDeLally Mar 16, 2020
6da07f6
Better typing
OoDeLally Mar 17, 2020
de86f6f
Better typing
OoDeLally Mar 17, 2020
7d0dd81
export Options
OoDeLally Mar 17, 2020
f40bc1c
Fixed typing
OoDeLally Mar 17, 2020
88e4e70
pretty
OoDeLally Mar 17, 2020
e9bb029
Linting
OoDeLally Mar 17, 2020
6789102
Linting again...
OoDeLally Mar 17, 2020
94eccbc
Better typing
OoDeLally Mar 17, 2020
2941964
typing, typing, typing...
OoDeLally Mar 17, 2020
0022f7e
typo
OoDeLally Mar 17, 2020
f4011c9
Test coverage for `after`
OoDeLally Mar 17, 2020
bdab59d
import the keyword list
OoDeLally Mar 18, 2020
c8b69fd
Dropped Keyword const type
OoDeLally Mar 18, 2020
80dcfe7
linting
OoDeLally Mar 18, 2020
1cee759
Added some tests to please the coverage check
OoDeLally Mar 18, 2020
b2973c4
Update packages/eslint-plugin/src/rules/keyword-spacing.ts
OoDeLally Apr 12, 2020
1a757d0
Update packages/eslint-plugin/typings/eslint-rules.d.ts
OoDeLally Apr 12, 2020
c9795c7
Update packages/eslint-plugin/src/rules/keyword-spacing.ts
OoDeLally Apr 12, 2020
4dd789e
Stuff
OoDeLally Apr 12, 2020
c26af99
Apparently these tools dont like force-push
OoDeLally Apr 13, 2020
0715791
Update keyword-spacing.md
bradzacher Apr 20, 2020
21f2c5f
Merge branch 'master' into keyword-spacing
bradzacher Apr 20, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/eslint-plugin/README.md
Expand Up @@ -183,6 +183,7 @@ In these cases, we create what we call an extension rule; a rule within our plug
| [`@typescript-eslint/default-param-last`](./docs/rules/default-param-last.md) | Enforce default parameters to be last | | | |
| [`@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/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>
2 changes: 2 additions & 0 deletions packages/eslint-plugin/src/configs/all.json
Expand Up @@ -22,6 +22,8 @@
"@typescript-eslint/func-call-spacing": "error",
"indent": "off",
"@typescript-eslint/indent": "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 @@ -118,6 +119,7 @@ export default {
'generic-type-naming': genericTypeNaming,
indent: indent,
'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,
bradzacher marked this conversation as resolved.
Show resolved Hide resolved
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;
},
};
},
});
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)
bradzacher marked this conversation as resolved.
Show resolved Hide resolved
//----------------------------------------------------------------------
{
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'),
},
],
});
79 changes: 79 additions & 0 deletions packages/eslint-plugin/typings/eslint-rules.d.ts
Expand Up @@ -142,6 +142,85 @@ declare module 'eslint/lib/rules/indent' {
export = rule;
}

declare module 'eslint/lib/rules/keyword-spacing' {
import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils';
import { RuleFunction } from '@typescript-eslint/experimental-utils/dist/ts-eslint';

type Options = [
{
before?: boolean;
after?: boolean;
overrides?: Record<
string,
{
before?: boolean;
after?: boolean;
}
>;
},
];
type MessageIds =
| 'expectedBefore'
| 'expectedAfter'
| 'unexpectedBefore'
| 'unexpectedAfter';

const rule: TSESLint.RuleModule<
MessageIds,
Options,
{
// Statements
DebuggerStatement: RuleFunction<TSESTree.DebuggerStatement>;
WithStatement: RuleFunction<TSESTree.WithStatement>;

// Statements - Control flow
BreakStatement: RuleFunction<TSESTree.BreakStatement>;
ContinueStatement: RuleFunction<TSESTree.ContinueStatement>;
ReturnStatement: RuleFunction<TSESTree.ReturnStatement>;
ThrowStatement: RuleFunction<TSESTree.ThrowStatement>;
TryStatement: RuleFunction<TSESTree.TryStatement>;

// Statements - Choice
IfStatement: RuleFunction<TSESTree.IfStatement>;
SwitchStatement: RuleFunction<TSESTree.Node>;
SwitchCase: RuleFunction<TSESTree.Node>;

// Statements - Loops
DoWhileStatement: RuleFunction<TSESTree.DoWhileStatement>;
ForInStatement: RuleFunction<TSESTree.ForInStatement>;
ForOfStatement: RuleFunction<TSESTree.ForOfStatement>;
ForStatement: RuleFunction<TSESTree.ForStatement>;
WhileStatement: RuleFunction<TSESTree.WhileStatement>;

// Statements - Declarations
ClassDeclaration: RuleFunction<TSESTree.ClassDeclaration>;
ExportNamedDeclaration: RuleFunction<TSESTree.ExportNamedDeclaration>;
ExportDefaultDeclaration: RuleFunction<TSESTree.ExportDefaultDeclaration>;
ExportAllDeclaration: RuleFunction<TSESTree.ExportAllDeclaration>;
FunctionDeclaration: RuleFunction<TSESTree.FunctionDeclaration>;
ImportDeclaration: RuleFunction<TSESTree.ImportDeclaration>;
VariableDeclaration: RuleFunction<TSESTree.VariableDeclaration>;

// Expressions
ArrowFunctionExpression: RuleFunction<TSESTree.ArrowFunctionExpression>;
AwaitExpression: RuleFunction<TSESTree.AwaitExpression>;
ClassExpression: RuleFunction<TSESTree.ClassExpression>;
FunctionExpression: RuleFunction<TSESTree.FunctionExpression>;
NewExpression: RuleFunction<TSESTree.NewExpression>;
Super: RuleFunction<TSESTree.Super>;
ThisExpression: RuleFunction<TSESTree.ThisExpression>;
UnaryExpression: RuleFunction<TSESTree.UnaryExpression>;
YieldExpression: RuleFunction<TSESTree.YieldExpression>;

// Others
ImportNamespaceSpecifier: RuleFunction<TSESTree.ImportNamespaceSpecifier>;
MethodDefinition: RuleFunction<TSESTree.MethodDefinition>;
Property: RuleFunction<TSESTree.Property>;
}
>;
export = rule;
}

declare module 'eslint/lib/rules/no-dupe-class-members' {
import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils';

Expand Down