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 func-call-spacing [extension] #448

Merged
merged 3 commits into from Apr 24, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion packages/eslint-plugin/README.md
Expand Up @@ -120,9 +120,10 @@ Then you should add `airbnb` (or `airbnb-base`) to your `extends` section of `.e
| [`@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: | | |
| [`@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: | | |
| [`@typescript-eslint/generic-type-naming`](./docs/rules/generic-type-naming.md) | Enforces naming of generic type variables | | | |
| [`@typescript-eslint/func-call-spacing`](./docs/rules/func-call-spacing.md) | Spacing between function identifiers and their invocations | | :wrench: | |
| [`@typescript-eslint/indent`](./docs/rules/indent.md) | Enforce consistent indentation (`indent` from TSLint) | :heavy_check_mark: | :wrench: | |
| [`@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: | | |
| [`@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: |
| [`@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: | |
| [`@typescript-eslint/member-naming`](./docs/rules/member-naming.md) | Enforces naming conventions for class members by visibility. | | | |
| [`@typescript-eslint/member-ordering`](./docs/rules/member-ordering.md) | Require a consistent member declaration order (`member-ordering` from TSLint) | | | |
| [`@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: | | |
Expand Down
26 changes: 26 additions & 0 deletions packages/eslint-plugin/docs/rules/func-call-spacing.md
@@ -0,0 +1,26 @@
# require or disallow spacing between function identifiers and their invocations (func-call-spacing)

When calling a function, developers may insert optional whitespace between the function’s name and the parentheses that invoke it.
This rule requires or disallows spaces between the function name and the opening parenthesis that calls it.

## Rule Details

This rule extends the base [eslint/func-call-spacing](https://eslint.org/docs/rules/func-call-spacing) 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
"func-call-spacing": "off",
"@typescript-eslint/func-call-spacing": ["error"]
}
```

## Options

See [eslint/func-call-spacing options](https://eslint.org/docs/rules/func-call-spacing#options).

<sup>Taken with ❤️ [from ESLint core](https://github.com/eslint/eslint/blob/master/docs/rules/func-call-spacing.md)</sup>
153 changes: 153 additions & 0 deletions packages/eslint-plugin/src/rules/func-call-spacing.ts
@@ -0,0 +1,153 @@
import { TSESTree } from '@typescript-eslint/typescript-estree';
import { isOpeningParenToken } from 'eslint-utils';
import * as util from '../util';

export type Options = [
'never' | 'always',
{
allowNewlines?: boolean;
}?
];
export type MessageIds = 'unexpected' | 'missing';

export default util.createRule<Options, MessageIds>({
name: 'func-call-spacing',
meta: {
type: 'layout',
docs: {
description:
'require or disallow spacing between function identifiers and their invocations',
category: 'Stylistic Issues',
recommended: false,
},
fixable: 'whitespace',
schema: {
anyOf: [
{
type: 'array',
items: [
{
enum: ['never'],
},
],
minItems: 0,
maxItems: 1,
},
{
type: 'array',
items: [
{
enum: ['always'],
},
{
type: 'object',
properties: {
allowNewlines: {
type: 'boolean',
},
},
additionalProperties: false,
},
],
minItems: 0,
maxItems: 2,
},
],
},

messages: {
unexpected:
'Unexpected space or newline between function name and paren.',
missing: 'Missing space between function name and paren.',
},
},
defaultOptions: ['never', {}],
create(context, [option, config]) {
const sourceCode = context.getSourceCode();
const text = sourceCode.getText();

/**
* Check if open space is present in a function name
* @param {ASTNode} node node to evaluate
* @returns {void}
* @private
*/
function checkSpacing(
node: TSESTree.CallExpression | TSESTree.NewExpression,
): void {
const closingParenToken = sourceCode.getLastToken(node)!;
const lastCalleeTokenWithoutPossibleParens = sourceCode.getLastToken(
node.typeParameters || node.callee,
)!;
const openingParenToken = sourceCode.getFirstTokenBetween(
lastCalleeTokenWithoutPossibleParens,
closingParenToken,
isOpeningParenToken,
);
if (!openingParenToken || openingParenToken.range[1] >= node.range[1]) {
// new expression with no parens...
return;
}
const lastCalleeToken = sourceCode.getTokenBefore(openingParenToken)!;

const textBetweenTokens = text
.slice(lastCalleeToken.range[1], openingParenToken.range[0])
.replace(/\/\*.*?\*\//gu, '');
const hasWhitespace = /\s/u.test(textBetweenTokens);
const hasNewline =
hasWhitespace && util.LINEBREAK_MATCHER.test(textBetweenTokens);

if (option === 'never') {
if (hasWhitespace) {
return context.report({
node,
loc: lastCalleeToken.loc.start,
messageId: 'unexpected',
fix(fixer) {
/*
* Only autofix if there is no newline
* https://github.com/eslint/eslint/issues/7787
*/
if (!hasNewline) {
return fixer.removeRange([
lastCalleeToken.range[1],
openingParenToken.range[0],
]);
}

return null;
},
});
}
} else {
if (!hasWhitespace) {
context.report({
node,
loc: lastCalleeToken.loc.start,
messageId: 'missing',
fix(fixer) {
return fixer.insertTextBefore(openingParenToken, ' ');
},
});
} else if (!config!.allowNewlines && hasNewline) {
context.report({
node,
loc: lastCalleeToken.loc.start,
messageId: 'unexpected',
fix(fixer) {
return fixer.replaceTextRange(
[lastCalleeToken.range[1], openingParenToken.range[0]],
' ',
);
},
});
}
}
}

return {
CallExpression: checkSpacing,
NewExpression: checkSpacing,
};
},
});
1 change: 1 addition & 0 deletions packages/eslint-plugin/src/util/astUtils.ts
@@ -0,0 +1 @@
export const LINEBREAK_MATCHER = /\r\n|[\r\n\u2028\u2029]/;
1 change: 1 addition & 0 deletions packages/eslint-plugin/src/util/index.ts
@@ -1,4 +1,5 @@
export * from './applyDefault';
export * from './astUtils';
export * from './createRule';
export * from './deepMerge';
export * from './getParserServices';
Expand Down