Skip to content

Commit 92e65ec

Browse files
bradzacherJamesHenry
authored andcommitted
feat(eslint-plugin): Add func-call-spacing (#448)
1 parent 8c88dff commit 92e65ec

File tree

7 files changed

+551
-2
lines changed

7 files changed

+551
-2
lines changed

packages/eslint-plugin/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,10 @@ Then you should add `airbnb` (or `airbnb-base`) to your `extends` section of `.e
120120
| [`@typescript-eslint/explicit-function-return-type`](./docs/rules/explicit-function-return-type.md) | Require explicit return types on functions and class methods | :heavy_check_mark: | | |
121121
| [`@typescript-eslint/explicit-member-accessibility`](./docs/rules/explicit-member-accessibility.md) | Require explicit accessibility modifiers on class properties and methods (`member-access` from TSLint) | :heavy_check_mark: | | |
122122
| [`@typescript-eslint/generic-type-naming`](./docs/rules/generic-type-naming.md) | Enforces naming of generic type variables | | | |
123+
| [`@typescript-eslint/func-call-spacing`](./docs/rules/func-call-spacing.md) | Spacing between function identifiers and their invocations | | :wrench: | |
123124
| [`@typescript-eslint/indent`](./docs/rules/indent.md) | Enforce consistent indentation (`indent` from TSLint) | :heavy_check_mark: | :wrench: | |
124125
| [`@typescript-eslint/interface-name-prefix`](./docs/rules/interface-name-prefix.md) | Require that interface names be prefixed with `I` (`interface-name` from TSLint) | :heavy_check_mark: | | |
125-
| [`@typescript-eslint/member-delimiter-style`](./docs/rules/member-delimiter-style.md) | Require a specific member delimiter style for interfaces and type literals | :heavy_check_mark: | :wrench: |
126+
| [`@typescript-eslint/member-delimiter-style`](./docs/rules/member-delimiter-style.md) | Require a specific member delimiter style for interfaces and type literals | :heavy_check_mark: | :wrench: | |
126127
| [`@typescript-eslint/member-naming`](./docs/rules/member-naming.md) | Enforces naming conventions for class members by visibility. | | | |
127128
| [`@typescript-eslint/member-ordering`](./docs/rules/member-ordering.md) | Require a consistent member declaration order (`member-ordering` from TSLint) | | | |
128129
| [`@typescript-eslint/no-angle-bracket-type-assertion`](./docs/rules/no-angle-bracket-type-assertion.md) | Enforces the use of `as Type` assertions instead of `<Type>` assertions (`no-angle-bracket-type-assertion` from TSLint) | :heavy_check_mark: | | |
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# require or disallow spacing between function identifiers and their invocations (func-call-spacing)
2+
3+
When calling a function, developers may insert optional whitespace between the function’s name and the parentheses that invoke it.
4+
This rule requires or disallows spaces between the function name and the opening parenthesis that calls it.
5+
6+
## Rule Details
7+
8+
This rule extends the base [eslint/func-call-spacing](https://eslint.org/docs/rules/func-call-spacing) rule.
9+
It supports all options and features of the base rule.
10+
This version adds support for generic type parameters on function calls.
11+
12+
## How to use
13+
14+
```cjson
15+
{
16+
// note you must disable the base rule as it can report incorrect errors
17+
"func-call-spacing": "off",
18+
"@typescript-eslint/func-call-spacing": ["error"]
19+
}
20+
```
21+
22+
## Options
23+
24+
See [eslint/func-call-spacing options](https://eslint.org/docs/rules/func-call-spacing#options).
25+
26+
<sup>Taken with ❤️ [from ESLint core](https://github.com/eslint/eslint/blob/master/docs/rules/func-call-spacing.md)</sup>
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import { TSESTree } from '@typescript-eslint/typescript-estree';
2+
import { isOpeningParenToken } from 'eslint-utils';
3+
import * as util from '../util';
4+
5+
export type Options = [
6+
'never' | 'always',
7+
{
8+
allowNewlines?: boolean;
9+
}?
10+
];
11+
export type MessageIds = 'unexpected' | 'missing';
12+
13+
export default util.createRule<Options, MessageIds>({
14+
name: 'func-call-spacing',
15+
meta: {
16+
type: 'layout',
17+
docs: {
18+
description:
19+
'require or disallow spacing between function identifiers and their invocations',
20+
category: 'Stylistic Issues',
21+
recommended: false,
22+
},
23+
fixable: 'whitespace',
24+
schema: {
25+
anyOf: [
26+
{
27+
type: 'array',
28+
items: [
29+
{
30+
enum: ['never'],
31+
},
32+
],
33+
minItems: 0,
34+
maxItems: 1,
35+
},
36+
{
37+
type: 'array',
38+
items: [
39+
{
40+
enum: ['always'],
41+
},
42+
{
43+
type: 'object',
44+
properties: {
45+
allowNewlines: {
46+
type: 'boolean',
47+
},
48+
},
49+
additionalProperties: false,
50+
},
51+
],
52+
minItems: 0,
53+
maxItems: 2,
54+
},
55+
],
56+
},
57+
58+
messages: {
59+
unexpected:
60+
'Unexpected space or newline between function name and paren.',
61+
missing: 'Missing space between function name and paren.',
62+
},
63+
},
64+
defaultOptions: ['never', {}],
65+
create(context, [option, config]) {
66+
const sourceCode = context.getSourceCode();
67+
const text = sourceCode.getText();
68+
69+
/**
70+
* Check if open space is present in a function name
71+
* @param {ASTNode} node node to evaluate
72+
* @returns {void}
73+
* @private
74+
*/
75+
function checkSpacing(
76+
node: TSESTree.CallExpression | TSESTree.NewExpression,
77+
): void {
78+
const closingParenToken = sourceCode.getLastToken(node)!;
79+
const lastCalleeTokenWithoutPossibleParens = sourceCode.getLastToken(
80+
node.typeParameters || node.callee,
81+
)!;
82+
const openingParenToken = sourceCode.getFirstTokenBetween(
83+
lastCalleeTokenWithoutPossibleParens,
84+
closingParenToken,
85+
isOpeningParenToken,
86+
);
87+
if (!openingParenToken || openingParenToken.range[1] >= node.range[1]) {
88+
// new expression with no parens...
89+
return;
90+
}
91+
const lastCalleeToken = sourceCode.getTokenBefore(openingParenToken)!;
92+
93+
const textBetweenTokens = text
94+
.slice(lastCalleeToken.range[1], openingParenToken.range[0])
95+
.replace(/\/\*.*?\*\//gu, '');
96+
const hasWhitespace = /\s/u.test(textBetweenTokens);
97+
const hasNewline =
98+
hasWhitespace && util.LINEBREAK_MATCHER.test(textBetweenTokens);
99+
100+
if (option === 'never') {
101+
if (hasWhitespace) {
102+
return context.report({
103+
node,
104+
loc: lastCalleeToken.loc.start,
105+
messageId: 'unexpected',
106+
fix(fixer) {
107+
/*
108+
* Only autofix if there is no newline
109+
* https://github.com/eslint/eslint/issues/7787
110+
*/
111+
if (!hasNewline) {
112+
return fixer.removeRange([
113+
lastCalleeToken.range[1],
114+
openingParenToken.range[0],
115+
]);
116+
}
117+
118+
return null;
119+
},
120+
});
121+
}
122+
} else {
123+
if (!hasWhitespace) {
124+
context.report({
125+
node,
126+
loc: lastCalleeToken.loc.start,
127+
messageId: 'missing',
128+
fix(fixer) {
129+
return fixer.insertTextBefore(openingParenToken, ' ');
130+
},
131+
});
132+
} else if (!config!.allowNewlines && hasNewline) {
133+
context.report({
134+
node,
135+
loc: lastCalleeToken.loc.start,
136+
messageId: 'unexpected',
137+
fix(fixer) {
138+
return fixer.replaceTextRange(
139+
[lastCalleeToken.range[1], openingParenToken.range[0]],
140+
' ',
141+
);
142+
},
143+
});
144+
}
145+
}
146+
}
147+
148+
return {
149+
CallExpression: checkSpacing,
150+
NewExpression: checkSpacing,
151+
};
152+
},
153+
});
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const LINEBREAK_MATCHER = /\r\n|[\r\n\u2028\u2029]/;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './applyDefault';
2+
export * from './astUtils';
23
export * from './createRule';
34
export * from './deepMerge';
45
export * from './getParserServices';

0 commit comments

Comments
 (0)