Skip to content

Commit d8b07a7

Browse files
Austarasbradzacher
authored andcommitted
feat(eslint-plugin): add space-before-function-paren [extension] (#924)
1 parent ca41dcf commit d8b07a7

File tree

7 files changed

+805
-0
lines changed

7 files changed

+805
-0
lines changed

packages/eslint-plugin/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ Then you should add `airbnb` (or `airbnb-base`) to your `extends` section of `.e
201201
| [`@typescript-eslint/require-await`](./docs/rules/require-await.md) | Disallow async functions which have no `await` expression | :heavy_check_mark: | | :thought_balloon: |
202202
| [`@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: |
203203
| [`@typescript-eslint/semi`](./docs/rules/semi.md) | Require or disallow semicolons instead of ASI | | :wrench: | |
204+
| [`@typescript-eslint/space-before-function-paren`](./docs/rules/space-before-function-paren.md) | enforce consistent spacing before `function` definition opening parenthesis | | :wrench: | |
204205
| [`@typescript-eslint/strict-boolean-expressions`](./docs/rules/strict-boolean-expressions.md) | Restricts the types allowed in boolean expressions | | | :thought_balloon: |
205206
| [`@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: | | |
206207
| [`@typescript-eslint/type-annotation-spacing`](./docs/rules/type-annotation-spacing.md) | Require consistent spacing around type annotations | :heavy_check_mark: | :wrench: | |
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Require or disallow a space before function parenthesis (space-before-function-paren)
2+
3+
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:
4+
5+
<!-- prettier-ignore -->
6+
```ts
7+
function withoutSpace (x) {
8+
// ...
9+
}
10+
11+
function withSpace (x) {
12+
// ...
13+
}
14+
15+
var anonymousWithoutSpace = function () {};
16+
17+
var anonymousWithSpace = function () {};
18+
```
19+
20+
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.
21+
22+
## Rule Details
23+
24+
This rule extends the base [eslint/func-call-spacing](https://eslint.org/docs/rules/space-before-function-paren) rule.
25+
It supports all options and features of the base rule.
26+
This version adds support for generic type parameters on function calls.
27+
28+
## How to use
29+
30+
```cjson
31+
{
32+
// note you must disable the base rule as it can report incorrect errors
33+
"space-before-function-paren": "off",
34+
"@typescript-eslint/space-before-function-paren": ["error"]
35+
}
36+
```
37+
38+
## Options
39+
40+
See [eslint/space-before-function-paren options](https://eslint.org/docs/rules/space-before-function-paren#options).
41+
42+
<sup>Taken with ❤️ [from ESLint core](https://github.com/eslint/eslint/blob/master/docs/rules/space-before-function-paren.md)</sup>

packages/eslint-plugin/src/configs/all.json

+2
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@
7777
"@typescript-eslint/restrict-plus-operands": "error",
7878
"semi": "off",
7979
"@typescript-eslint/semi": "error",
80+
"space-before-function-paren": "off",
81+
"@typescript-eslint/space-before-function-paren": "error",
8082
"@typescript-eslint/strict-boolean-expressions": "error",
8183
"@typescript-eslint/triple-slash-reference": "error",
8284
"@typescript-eslint/type-annotation-spacing": "error",

packages/eslint-plugin/src/rules/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ import requireArraySortCompare from './require-array-sort-compare';
5858
import requireAwait from './require-await';
5959
import restrictPlusOperands from './restrict-plus-operands';
6060
import semi from './semi';
61+
import spaceBeforeFunctionParen from './space-before-function-paren';
6162
import strictBooleanExpressions from './strict-boolean-expressions';
6263
import tripleSlashReference from './triple-slash-reference';
6364
import typeAnnotationSpacing from './type-annotation-spacing';
@@ -128,6 +129,7 @@ export default {
128129
'require-await': requireAwait,
129130
'restrict-plus-operands': restrictPlusOperands,
130131
semi: semi,
132+
'space-before-function-paren': spaceBeforeFunctionParen,
131133
'strict-boolean-expressions': strictBooleanExpressions,
132134
'triple-slash-reference': tripleSlashReference,
133135
'type-annotation-spacing': typeAnnotationSpacing,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import {
2+
TSESTree,
3+
AST_NODE_TYPES,
4+
} from '@typescript-eslint/experimental-utils';
5+
import { isOpeningParenToken } from 'eslint-utils';
6+
import * as util from '../util';
7+
8+
type Option = 'never' | 'always';
9+
type FuncOption = Option | 'ignore';
10+
11+
export type Options = [
12+
13+
| Option
14+
| Partial<{
15+
anonymous: FuncOption;
16+
named: FuncOption;
17+
asyncArrow: FuncOption;
18+
}>,
19+
];
20+
export type MessageIds = 'unexpected' | 'missing';
21+
22+
export default util.createRule<Options, MessageIds>({
23+
name: 'space-before-function-paren',
24+
meta: {
25+
type: 'layout',
26+
docs: {
27+
description:
28+
'enforce consistent spacing before `function` definition opening parenthesis',
29+
category: 'Stylistic Issues',
30+
recommended: false,
31+
},
32+
fixable: 'whitespace',
33+
schema: [
34+
{
35+
oneOf: [
36+
{
37+
enum: ['always', 'never'],
38+
},
39+
{
40+
type: 'object',
41+
properties: {
42+
anonymous: {
43+
enum: ['always', 'never', 'ignore'],
44+
},
45+
named: {
46+
enum: ['always', 'never', 'ignore'],
47+
},
48+
asyncArrow: {
49+
enum: ['always', 'never', 'ignore'],
50+
},
51+
},
52+
additionalProperties: false,
53+
},
54+
],
55+
},
56+
],
57+
messages: {
58+
unexpected: 'Unexpected space before function parentheses.',
59+
missing: 'Missing space before function parentheses.',
60+
},
61+
},
62+
defaultOptions: ['always'],
63+
64+
create(context) {
65+
const sourceCode = context.getSourceCode();
66+
const baseConfig =
67+
typeof context.options[0] === 'string' ? context.options[0] : 'always';
68+
const overrideConfig =
69+
typeof context.options[0] === 'object' ? context.options[0] : {};
70+
71+
/**
72+
* Determines whether a function has a name.
73+
* @param {ASTNode} node The function node.
74+
* @returns {boolean} Whether the function has a name.
75+
*/
76+
function isNamedFunction(
77+
node:
78+
| TSESTree.ArrowFunctionExpression
79+
| TSESTree.FunctionDeclaration
80+
| TSESTree.FunctionExpression,
81+
): boolean {
82+
if (node.id) {
83+
return true;
84+
}
85+
86+
const parent = node.parent!;
87+
88+
return (
89+
parent.type === 'MethodDefinition' ||
90+
(parent.type === 'Property' &&
91+
(parent.kind === 'get' || parent.kind === 'set' || parent.method))
92+
);
93+
}
94+
95+
/**
96+
* Gets the config for a given function
97+
* @param {ASTNode} node The function node
98+
* @returns {string} "always", "never", or "ignore"
99+
*/
100+
function getConfigForFunction(
101+
node:
102+
| TSESTree.ArrowFunctionExpression
103+
| TSESTree.FunctionDeclaration
104+
| TSESTree.FunctionExpression,
105+
): FuncOption {
106+
if (node.type === AST_NODE_TYPES.ArrowFunctionExpression) {
107+
// Always ignore non-async functions and arrow functions without parens, e.g. async foo => bar
108+
if (
109+
node.async &&
110+
isOpeningParenToken(sourceCode.getFirstToken(node, { skip: 1 })!)
111+
) {
112+
return overrideConfig.asyncArrow || baseConfig;
113+
}
114+
} else if (isNamedFunction(node)) {
115+
return overrideConfig.named || baseConfig;
116+
117+
// `generator-star-spacing` should warn anonymous generators. E.g. `function* () {}`
118+
} else if (!node.generator) {
119+
return overrideConfig.anonymous || baseConfig;
120+
}
121+
122+
return 'ignore';
123+
}
124+
125+
/**
126+
* Checks the parens of a function node
127+
* @param {ASTNode} node A function node
128+
* @returns {void}
129+
*/
130+
function checkFunction(
131+
node:
132+
| TSESTree.ArrowFunctionExpression
133+
| TSESTree.FunctionDeclaration
134+
| TSESTree.FunctionExpression,
135+
): void {
136+
const functionConfig = getConfigForFunction(node);
137+
138+
if (functionConfig === 'ignore') {
139+
return;
140+
}
141+
142+
let leftToken: TSESTree.Token, rightToken: TSESTree.Token;
143+
if (node.typeParameters) {
144+
leftToken = sourceCode.getLastToken(node.typeParameters)!;
145+
rightToken = sourceCode.getTokenAfter(leftToken)!;
146+
} else {
147+
rightToken = sourceCode.getFirstToken(node, isOpeningParenToken)!;
148+
leftToken = sourceCode.getTokenBefore(rightToken)!;
149+
}
150+
const hasSpacing = sourceCode.isSpaceBetweenTokens(leftToken, rightToken);
151+
152+
if (hasSpacing && functionConfig === 'never') {
153+
context.report({
154+
node,
155+
loc: leftToken.loc.end,
156+
messageId: 'unexpected',
157+
fix: fixer =>
158+
fixer.removeRange([leftToken.range[1], rightToken.range[0]]),
159+
});
160+
} else if (
161+
!hasSpacing &&
162+
functionConfig === 'always' &&
163+
(!node.typeParameters || node.id)
164+
) {
165+
context.report({
166+
node,
167+
loc: leftToken.loc.end,
168+
messageId: 'missing',
169+
fix: fixer => fixer.insertTextAfter(leftToken, ' '),
170+
});
171+
}
172+
}
173+
174+
return {
175+
ArrowFunctionExpression: checkFunction,
176+
FunctionDeclaration: checkFunction,
177+
FunctionExpression: checkFunction,
178+
};
179+
},
180+
});

0 commit comments

Comments
 (0)