Skip to content

Commit

Permalink
feat(eslint-plugin): add space-before-function-paren [extension] (#924)
Browse files Browse the repository at this point in the history
  • Loading branch information
Austaras authored and bradzacher committed Nov 14, 2019
1 parent ca41dcf commit d8b07a7
Show file tree
Hide file tree
Showing 7 changed files with 805 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/eslint-plugin/README.md
Expand Up @@ -201,6 +201,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
@@ -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
Expand Up @@ -77,6 +77,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",
"@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
Expand Up @@ -58,6 +58,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 @@ -128,6 +129,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
@@ -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
| 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,
};
},
});

0 comments on commit d8b07a7

Please sign in to comment.