Skip to content

Commit

Permalink
feat: implement arrow-parens to support type parameters (#344) (#417)
Browse files Browse the repository at this point in the history
  • Loading branch information
lyleunderwood authored and gajus committed Jul 18, 2019
1 parent 9b37c35 commit 94a9e8b
Show file tree
Hide file tree
Showing 8 changed files with 531 additions and 2 deletions.
1 change: 1 addition & 0 deletions .README/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ When `true`, only checks files with a [`@flow` annotation](http://flowtype.org/d

{"gitdown": "include", "file": "./rules/array-style-complex-type.md"}
{"gitdown": "include", "file": "./rules/array-style-simple-type.md"}
{"gitdown": "include", "file": "./rules/arrow-parens.md"}
{"gitdown": "include", "file": "./rules/boolean-style.md"}
{"gitdown": "include", "file": "./rules/define-flow-type.md"}
{"gitdown": "include", "file": "./rules/delimiter-dangle.md"}
Expand Down
18 changes: 18 additions & 0 deletions .README/rules/arrow-parens.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
### `arrow-parens`

_The `--fix` option on the command line automatically fixes problems reported by this rule._

Enforces the consistent use of parentheses in arrow functions.

This rule has a string option and an object one.

String options are:

- `"always"` (default) requires parens around arguments in all cases.
- `"as-needed"` enforces no braces where they can be omitted.

Object properties for variants of the `"as-needed"` option:

- `"requireForBlockBody": true` modifies the as-needed rule in order to require parens if the function body is in an instructions block (surrounded by braces).

<!-- assertions arrowParens -->
4 changes: 2 additions & 2 deletions src/configs/recommended.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@
"flowtype/no-types-missing-file-annotation": 2,
"flowtype/no-weak-types": 0,
"flowtype/require-parameter-type": 0,
"flowtype/require-readonly-react-props": 0,
"flowtype/require-return-type": 0,
"flowtype/require-valid-file-annotation": 0,
"flowtype/require-readonly-react-props": 0,
"flowtype/semi": 0,
"flowtype/space-after-type-colon": [
2,
Expand All @@ -46,4 +46,4 @@
"onlyFilesWithFlowAnnotation": false
}
}
}
}
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,13 @@ import unionIntersectionSpacing from './rules/unionIntersectionSpacing';
import useFlowType from './rules/useFlowType';
import validSyntax from './rules/validSyntax';
import spreadExactType from './rules/spreadExactType';
import arrowParens from './rules/arrowParens';
import {checkFlowFileAnnotation} from './utilities';

const rules = {
'array-style-complex-type': arrayStyleComplexType,
'array-style-simple-type': arrayStyleSimpleType,
'arrow-parens': arrowParens,
'boolean-style': booleanStyle,
'define-flow-type': defineFlowType,
'delimiter-dangle': delimiterDangle,
Expand Down
169 changes: 169 additions & 0 deletions src/rules/arrowParens.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
const getLocation = (node) => {
return {
end: node.params[node.params.length - 1].loc.end,
start: node.params[0].loc.start
};
};

const isOpeningParenToken = (token) => {
return token.value === '(' && token.type === 'Punctuator';
};

const isClosingParenToken = (token) => {
return token.value === ')' && token.type === 'Punctuator';
};

export default {
create (context) {
const asNeeded = context.options[0] === 'as-needed';
const requireForBlockBody = asNeeded && context.options[1] && context.options[1].requireForBlockBody === true;

const sourceCode = context.getSourceCode();

// Determines whether a arrow function argument end with `)`
// eslint-disable-next-line complexity
const parens = (node) => {
const isAsync = node.async;
const firstTokenOfParam = sourceCode.getFirstToken(node, isAsync ? 1 : 0);

// Remove the parenthesis around a parameter
const fixParamsWithParenthesis = (fixer) => {
const paramToken = sourceCode.getTokenAfter(firstTokenOfParam);

/*
* ES8 allows Trailing commas in function parameter lists and calls
* https://github.com/eslint/eslint/issues/8834
*/
const closingParenToken = sourceCode.getTokenAfter(paramToken, isClosingParenToken);
const asyncToken = isAsync ? sourceCode.getTokenBefore(firstTokenOfParam) : null;
const shouldAddSpaceForAsync = asyncToken && asyncToken.range[1] === firstTokenOfParam.range[0];

return fixer.replaceTextRange([
firstTokenOfParam.range[0],
closingParenToken.range[1]
], `${shouldAddSpaceForAsync ? ' ' : ''}${paramToken.value}`);
};

// Type parameters without an opening paren is always a parse error, and
// can therefore be safely ignored.
if (node.typeParameters) {
return;
}

// "as-needed", { "requireForBlockBody": true }: x => x
if (
requireForBlockBody &&
node.params.length === 1 &&
node.params[0].type === 'Identifier' &&
!node.params[0].typeAnnotation &&
node.body.type !== 'BlockStatement' &&
!node.returnType
) {
if (isOpeningParenToken(firstTokenOfParam)) {
context.report({
fix: fixParamsWithParenthesis,
loc: getLocation(node),
messageId: 'unexpectedParensInline',
node
});
}

return;
}

if (
requireForBlockBody &&
node.body.type === 'BlockStatement'
) {
if (!isOpeningParenToken(firstTokenOfParam)) {
context.report({
fix (fixer) {
return fixer.replaceText(firstTokenOfParam, `(${firstTokenOfParam.value})`);
},
loc: getLocation(node),
messageId: 'expectedParensBlock',
node
});
}

return;
}

// "as-needed": x => x
if (asNeeded &&
node.params.length === 1 &&
node.params[0].type === 'Identifier' &&
!node.params[0].typeAnnotation &&
!node.returnType
) {
if (isOpeningParenToken(firstTokenOfParam)) {
context.report({
fix: fixParamsWithParenthesis,
loc: getLocation(node),
messageId: 'unexpectedParens',
node
});
}

return;
}

if (firstTokenOfParam.type === 'Identifier') {
const after = sourceCode.getTokenAfter(firstTokenOfParam);

// (x) => x
if (after.value !== ')') {
context.report({
fix (fixer) {
return fixer.replaceText(firstTokenOfParam, `(${firstTokenOfParam.value})`);
},
loc: getLocation(node),
messageId: 'expectedParens',
node
});
}
}
};

return {
ArrowFunctionExpression: parens
};
},

meta: {
docs: {
category: 'ECMAScript 6',
description: 'require parentheses around arrow function arguments',
recommended: false,
url: 'https://eslint.org/docs/rules/arrow-parens'
},

fixable: 'code',

messages: {
expectedParens: 'Expected parentheses around arrow function argument.',
expectedParensBlock: 'Expected parentheses around arrow function argument having a body with curly braces.',

unexpectedParens: 'Unexpected parentheses around single function argument.',
unexpectedParensInline: 'Unexpected parentheses around single function argument having a body with no curly braces.'
},

type: 'layout'
},

schema: [
{
enum: ['always', 'as-needed']
},
{
additionalProperties: false,
properties: {
requireForBlockBody: {
default: false,
type: 'boolean'
}
},
type: 'object'
}
]
};
1 change: 1 addition & 0 deletions src/utilities/isFlowFile.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import isFlowFileAnnotation from './isFlowFileAnnotation';
/* eslint-disable flowtype/require-valid-file-annotation */
/**
* Checks whether a file has an @flow or @noflow annotation.
*
* @param context
* @param [strict] - By default, the function returns true if the file starts with @flow but not if it
* starts by @noflow. When the strict flag is set to false, the function returns true if the flag has @noflow also.
Expand Down

0 comments on commit 94a9e8b

Please sign in to comment.