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 space-before-function-paren [extension] #924

Merged
merged 10 commits into from
Nov 14, 2019
1 change: 1 addition & 0 deletions packages/eslint-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ Then you should add `airbnb` (or `airbnb-base`) to your `extends` section of `.e
| [`@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: |
| [`@typescript-eslint/semi`](./docs/rules/semi.md) | Require or disallow semicolons instead of ASI | | :wrench: | |
| [`@typescript-eslint/space-before-function-paren`](./docs/rules/space-before-function-paren.md) | enforce consistent spacing before `function` definition opening parenthesis | | :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 | :heavy_check_mark: | | |
| [`@typescript-eslint/type-annotation-spacing`](./docs/rules/type-annotation-spacing.md) | Require consistent spacing around type annotations | :heavy_check_mark: | :wrench: | |
Expand Down
42 changes: 42 additions & 0 deletions packages/eslint-plugin/docs/rules/space-before-function-paren.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Require or disallow a space before function parenthesis (space-before-function-paren)

When formatting a function, whitespace is allowed between the function name or `function` keyword and the opening paren. Named functions also require a space between the `function` keyword and the function name, but anonymous functions require no whitespace. For example:

<!-- prettier-ignore -->
```ts
function withoutSpace (x) {
// ...
}

function withSpace (x) {
// ...
}

var anonymousWithoutSpace = function () {};

var anonymousWithSpace = function () {};
```

Style guides may require a space after the `function` keyword for anonymous functions, while others specify no whitespace. Similarly, the space after a function name may or may not be required.

## Rule Details

This rule extends the base [eslint/func-call-spacing](https://eslint.org/docs/rules/space-before-function-paren) rule.
It supports all options and features of the base 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
"space-before-function-paren": "off",
"@typescript-eslint/space-before-function-paren": ["error"]
}
```

## Options

See [eslint/space-before-function-paren options](https://eslint.org/docs/rules/space-before-function-paren#options).

<sup>Taken with ❤️ [from ESLint core](https://github.com/eslint/eslint/blob/master/docs/rules/space-before-function-paren.md)</sup>
2 changes: 2 additions & 0 deletions packages/eslint-plugin/src/configs/all.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@
"@typescript-eslint/restrict-plus-operands": "error",
"semi": "off",
"@typescript-eslint/semi": "error",
"space-before-function-paren": "off",
"@typescript-eslint/space-before-function-paren": "error",
Austaras marked this conversation as resolved.
Show resolved Hide resolved
"@typescript-eslint/strict-boolean-expressions": "error",
"@typescript-eslint/triple-slash-reference": "error",
"@typescript-eslint/type-annotation-spacing": "error",
Expand Down
2 changes: 2 additions & 0 deletions packages/eslint-plugin/src/rules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import requireArraySortCompare from './require-array-sort-compare';
import requireAwait from './require-await';
import restrictPlusOperands from './restrict-plus-operands';
import semi from './semi';
import spaceBeforeFunctionParen from './space-before-function-paren';
import strictBooleanExpressions from './strict-boolean-expressions';
import tripleSlashReference from './triple-slash-reference';
import typeAnnotationSpacing from './type-annotation-spacing';
Expand Down Expand Up @@ -122,6 +123,7 @@ export default {
'require-await': requireAwait,
'restrict-plus-operands': restrictPlusOperands,
semi: semi,
'space-before-function-paren': spaceBeforeFunctionParen,
'strict-boolean-expressions': strictBooleanExpressions,
'triple-slash-reference': tripleSlashReference,
'type-annotation-spacing': typeAnnotationSpacing,
Expand Down
180 changes: 180 additions & 0 deletions packages/eslint-plugin/src/rules/space-before-function-paren.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import {
TSESTree,
AST_NODE_TYPES,
} from '@typescript-eslint/experimental-utils';
import { isOpeningParenToken } from 'eslint-utils';
import * as util from '../util';

type Option = 'never' | 'always';
type FuncOption = Option | 'ignore';

export type Options = [

| Option
Comment on lines +11 to +13
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit formatting - this was a bug in prettier that's since been fixed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe you should update prettier in yarn.lock first or test for format will fail

| Partial<{
anonymous: FuncOption;
named: FuncOption;
asyncArrow: FuncOption;
}>,
];
export type MessageIds = 'unexpected' | 'missing';

export default util.createRule<Options, MessageIds>({
name: 'space-before-function-paren',
meta: {
type: 'layout',
docs: {
description:
'enforce consistent spacing before `function` definition opening parenthesis',
category: 'Stylistic Issues',
recommended: false,
},
fixable: 'whitespace',
schema: [
{
oneOf: [
{
enum: ['always', 'never'],
},
{
type: 'object',
properties: {
anonymous: {
enum: ['always', 'never', 'ignore'],
},
named: {
enum: ['always', 'never', 'ignore'],
},
asyncArrow: {
enum: ['always', 'never', 'ignore'],
},
},
additionalProperties: false,
},
],
},
],
messages: {
unexpected: 'Unexpected space before function parentheses.',
missing: 'Missing space before function parentheses.',
},
},
defaultOptions: ['always'],

create(context) {
const sourceCode = context.getSourceCode();
const baseConfig =
typeof context.options[0] === 'string' ? context.options[0] : 'always';
const overrideConfig =
typeof context.options[0] === 'object' ? context.options[0] : {};

/**
* Determines whether a function has a name.
* @param {ASTNode} node The function node.
* @returns {boolean} Whether the function has a name.
*/
function isNamedFunction(
node:
| TSESTree.ArrowFunctionExpression
| TSESTree.FunctionDeclaration
| TSESTree.FunctionExpression,
): boolean {
if (node.id) {
return true;
}

const parent = node.parent!;

return (
parent.type === 'MethodDefinition' ||
(parent.type === 'Property' &&
(parent.kind === 'get' || parent.kind === 'set' || parent.method))
);
}

/**
* Gets the config for a given function
* @param {ASTNode} node The function node
* @returns {string} "always", "never", or "ignore"
*/
function getConfigForFunction(
node:
| TSESTree.ArrowFunctionExpression
| TSESTree.FunctionDeclaration
| TSESTree.FunctionExpression,
): FuncOption {
if (node.type === AST_NODE_TYPES.ArrowFunctionExpression) {
// Always ignore non-async functions and arrow functions without parens, e.g. async foo => bar
if (
node.async &&
isOpeningParenToken(sourceCode.getFirstToken(node, { skip: 1 })!)
) {
return overrideConfig.asyncArrow || baseConfig;
}
} else if (isNamedFunction(node)) {
return overrideConfig.named || baseConfig;

// `generator-star-spacing` should warn anonymous generators. E.g. `function* () {}`
} else if (!node.generator) {
return overrideConfig.anonymous || baseConfig;
}

return 'ignore';
}

/**
* Checks the parens of a function node
* @param {ASTNode} node A function node
* @returns {void}
*/
function checkFunction(
node:
| TSESTree.ArrowFunctionExpression
| TSESTree.FunctionDeclaration
| TSESTree.FunctionExpression,
): void {
const functionConfig = getConfigForFunction(node);

if (functionConfig === 'ignore') {
return;
}

let leftToken: TSESTree.Token, rightToken: TSESTree.Token;
if (node.typeParameters) {
leftToken = sourceCode.getLastToken(node.typeParameters)!;
rightToken = sourceCode.getTokenAfter(leftToken)!;
} else {
rightToken = sourceCode.getFirstToken(node, isOpeningParenToken)!;
leftToken = sourceCode.getTokenBefore(rightToken)!;
}
const hasSpacing = sourceCode.isSpaceBetweenTokens(leftToken, rightToken);

if (hasSpacing && functionConfig === 'never') {
context.report({
node,
loc: leftToken.loc.end,
messageId: 'unexpected',
fix: fixer =>
fixer.removeRange([leftToken.range[1], rightToken.range[0]]),
});
} else if (
!hasSpacing &&
functionConfig === 'always' &&
(!node.typeParameters || node.id)
) {
context.report({
node,
loc: leftToken.loc.end,
messageId: 'missing',
fix: fixer => fixer.insertTextAfter(leftToken, ' '),
});
}
}

return {
ArrowFunctionExpression: checkFunction,
FunctionDeclaration: checkFunction,
FunctionExpression: checkFunction,
};
},
});