diff --git a/docs/options.md b/docs/options.md index 8a9cfee7a..b3d83cfd4 100644 --- a/docs/options.md +++ b/docs/options.md @@ -96,6 +96,19 @@ Put the `>` of a multi-line JSX element at the end of the last line instead of b | ------- | ------------------------- | ---------------------------- | | `false` | `--jsx-bracket-same-line` | `jsxBracketSameLine: ` | +# Arrow Function Parentheses + +Include parentheses around a sole arrow function parameter. + +Valid options: + +* `"avoid"` - Omit parens when possible. Example: `x => x` +* `"always"` - Always include parens. Example: `(x) => x` + +| Default | CLI Override | API Override | +| --------- | --------------------------------------------------------- | ------------------------------------------------------- | +| `"avoid"` | --arrow-function-parens | arrowFunctionParentheses: "" | + ## Range Format only a segment of a file. diff --git a/src/cli-constant.js b/src/cli-constant.js index 92ce7e371..376c718b9 100644 --- a/src/cli-constant.js +++ b/src/cli-constant.js @@ -77,6 +77,23 @@ const categoryOrder = [ * Note: The options below are sorted alphabetically. */ const detailedOptions = normalizeDetailedOptions({ + "arrow-function-parentheses": { + type: "choice", + category: CATEGORY_FORMAT, + forwardToApi: true, + description: "Include parentheses around a sole arrow function parameter.", + default: "avoid", + choices: [ + { + value: "avoid", + description: "Omit parens when possible. Example: `x => x`" + }, + { + value: "always", + description: "Always include parens. Example: `(x) => x`" + } + ] + }, "bracket-spacing": { type: "boolean", category: CATEGORY_FORMAT, diff --git a/src/options.js b/src/options.js index 19c34ab80..87ba34028 100644 --- a/src/options.js +++ b/src/options.js @@ -21,7 +21,8 @@ const defaults = { insertPragma: false, requirePragma: false, semi: true, - proseWrap: true + proseWrap: true, + arrowFunctionParentheses: "avoid" }; const exampleConfig = Object.assign({}, defaults, { diff --git a/tests/arrow-call/__snapshots__/jsfmt.spec.js.snap b/tests/arrow-call/__snapshots__/jsfmt.spec.js.snap index 0597d87a7..006da744c 100644 --- a/tests/arrow-call/__snapshots__/jsfmt.spec.js.snap +++ b/tests/arrow-call/__snapshots__/jsfmt.spec.js.snap @@ -189,3 +189,98 @@ const composition = (ViewComponent, ContainerComponent) => }; `; + +exports[`arrow_call.js 3`] = ` +const testResults = results.testResults.map(testResult => + formatResult(testResult, formatter, reporter) +); + +it('mocks regexp instances', () => { + expect( + () => moduleMocker.generateFromMetadata(moduleMocker.getMetadata(/a/)), + ).not.toThrow(); +}); + +expect(() => asyncRequest({ url: "/test-endpoint" })) + .toThrowError(/Required parameter/); + +expect(() => asyncRequest({ url: "/test-endpoint-but-with-a-long-url" })) + .toThrowError(/Required parameter/); + +expect(() => asyncRequest({ url: "/test-endpoint-but-with-a-suuuuuuuuper-long-url" })) + .toThrowError(/Required parameter/); + +expect(() => asyncRequest({ type: "foo", url: "/test-endpoint" })) + .not.toThrowError(); + +expect(() => asyncRequest({ type: "foo", url: "/test-endpoint-but-with-a-long-url" })) + .not.toThrowError(); + +const a = Observable + .fromPromise(axiosInstance.post('/carts/mine')) + .map((response) => response.data) + +const b = Observable.fromPromise(axiosInstance.get(url)) + .map((response) => response.data) + +func( + veryLoooooooooooooooooooooooongName, + veryLooooooooooooooooooooooooongName => + veryLoooooooooooooooongName.something() +); + +const composition = (ViewComponent, ContainerComponent) => + class extends React.Component { + static propTypes = {}; + }; +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +const testResults = results.testResults.map(testResult => + formatResult(testResult, formatter, reporter) +); + +it("mocks regexp instances", () => { + expect(() => + moduleMocker.generateFromMetadata(moduleMocker.getMetadata(/a/)) + ).not.toThrow(); +}); + +expect(() => asyncRequest({ url: "/test-endpoint" })).toThrowError( + /Required parameter/ +); + +expect(() => + asyncRequest({ url: "/test-endpoint-but-with-a-long-url" }) +).toThrowError(/Required parameter/); + +expect(() => + asyncRequest({ url: "/test-endpoint-but-with-a-suuuuuuuuper-long-url" }) +).toThrowError(/Required parameter/); + +expect(() => + asyncRequest({ type: "foo", url: "/test-endpoint" }) +).not.toThrowError(); + +expect(() => + asyncRequest({ type: "foo", url: "/test-endpoint-but-with-a-long-url" }) +).not.toThrowError(); + +const a = Observable.fromPromise(axiosInstance.post("/carts/mine")).map( + response => response.data +); + +const b = Observable.fromPromise(axiosInstance.get(url)).map( + response => response.data +); + +func( + veryLoooooooooooooooooooooooongName, + veryLooooooooooooooooooooooooongName => + veryLoooooooooooooooongName.something() +); + +const composition = (ViewComponent, ContainerComponent) => + class extends React.Component { + static propTypes = {}; + }; + +`; diff --git a/tests/arrow-call/jsfmt.spec.js b/tests/arrow-call/jsfmt.spec.js index 4e02696c9..db45d9868 100644 --- a/tests/arrow-call/jsfmt.spec.js +++ b/tests/arrow-call/jsfmt.spec.js @@ -1,2 +1,3 @@ run_spec(__dirname, null, ["typescript"]); run_spec(__dirname, { trailingComma: "all" }, ["typescript"]); +run_spec(__dirname, { arrowFunctionParentheses: "always" }, ["typescript"]); diff --git a/tests/arrows/__snapshots__/jsfmt.spec.js.snap b/tests/arrows/__snapshots__/jsfmt.spec.js.snap index 065c38271..c4794e33f 100644 --- a/tests/arrows/__snapshots__/jsfmt.spec.js.snap +++ b/tests/arrows/__snapshots__/jsfmt.spec.js.snap @@ -32,6 +32,10 @@ x => (y = z); x => (y += z); f(a => ({})) + 1; (a => ({})) || 0; +a = b => c; +a = b => { + return c +}; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ (a => {}).length; typeof (() => {}); @@ -65,6 +69,86 @@ x => (y = z); x => (y += z); f(a => ({})) + 1; (a => ({})) || 0; +a = b => c; +a = b => { + return c; +}; + +`; + +exports[`arrow_function_expression.js 2`] = ` +(a => {}).length +typeof (() => {}); +export default (() => {})(); +(() => {})()\`\`; +(() => {})\`\`; +new (() => {}); +if ((() => {}) ? 1 : 0) {} +let f = () => ({}()) +let a = () => ({} instanceof a); +a = () => ({} && a); +a = () => ({}() && a); +a = () => ({} && a && b); +a = () => ({} + a); +a = () => ({}()() && a); +a = () => ({}.b && a); +a = () => ({}[b] && a); +a = () => ({}\`\` && a); +a = () => ({} = 0); +a = () => ({}, a); +a => a instanceof {}; +a => ({}().b && 0) +a => ({}().c = 0) +x => ({}()()) +x => ({}()\`\`) +x => ({}().b); +a = b => c; +a = (b?) => c; +x => (y = z); +x => (y += z); +f(a => ({})) + 1; +(a => ({})) || 0; +a = b => c; +a = b => { + return c +}; +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +(a => {}).length; +typeof (() => {}); +export default (() => {})(); +(() => {})()\`\`; +(() => {})\`\`; +new (() => {})(); +if ((() => {}) ? 1 : 0) { +} +let f = () => ({}()); +let a = () => ({} instanceof a); +a = () => ({} && a); +a = () => ({}() && a); +a = () => ({} && a && b); +a = () => ({} + a); +a = () => ({}()() && a); +a = () => ({}.b && a); +a = () => ({}[b] && a); +a = () => ({}\`\` && a); +a = () => ({} = 0); +a = () => ({}, a); +a => a instanceof {}; +a => ({}().b && 0); +a => ({}().c = 0); +x => ({}()()); +x => ({}()\`\`); +x => ({}().b); +a = b => c; +a = (b?) => c; +x => (y = z); +x => (y += z); +f(a => ({})) + 1; +(a => ({})) || 0; +a = b => c; +a = b => { + return c; +}; `; @@ -75,6 +159,13 @@ a = () => ({} = this); `; +exports[`block_like.js 2`] = ` +a = () => ({} = this); +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +a = () => ({} = this); + +`; + exports[`call.js 1`] = ` Seq(typeDef.interface.groups).forEach(group => Seq(group.members).forEach((member, memberName) => @@ -248,6 +339,179 @@ fooooooooooooooooooooooooooooooooooooooooooooooooooo(action => next => `; +exports[`call.js 2`] = ` +Seq(typeDef.interface.groups).forEach(group => + Seq(group.members).forEach((member, memberName) => + markdownDoc( + member.doc, + { typePath: typePath.concat(memberName.slice(1)), + signatures: member.signatures } + ) + ) +) + +const promiseFromCallback = fn => + new Promise((resolve, reject) => + fn((err, result) => { + if (err) return reject(err); + return resolve(result); + }) + ); + +runtimeAgent.getProperties( + objectId, + false, // ownProperties + false, // accessorPropertiesOnly + false, // generatePreview + (error, properties, internalProperties) => { + return 1 + }, +); + +function render() { + return ( + + this.setState({progress: Math.round(100 * e.nativeEvent.loaded / e.nativeEvent.total)})} + /> + + ); +} + +function render() { + return ( + + + this.setState({ + progress: Math.round( + 100 * e.nativeEvent.loaded / e.nativeEvent.total, + ), + })} + /> + + ); +} + +function render() { + return ( + + + this.setState({ + progress: Math.round( + 100 * e.nativeEvent.loaded / e.nativeEvent.total, + ), + })} + /> + + ); +} + +jest.mock( + '../SearchSource', + () => class { + findMatchingTests(pattern) { + return {paths: []}; + } + }, +); + +fooooooooooooooooooooooooooooooooooooooooooooooooooo(action => next => + dispatch(action), +); +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Seq(typeDef.interface.groups).forEach(group => + Seq(group.members).forEach((member, memberName) => + markdownDoc(member.doc, { + typePath: typePath.concat(memberName.slice(1)), + signatures: member.signatures + }) + ) +); + +const promiseFromCallback = fn => + new Promise((resolve, reject) => + fn((err, result) => { + if (err) return reject(err); + return resolve(result); + }) + ); + +runtimeAgent.getProperties( + objectId, + false, // ownProperties + false, // accessorPropertiesOnly + false, // generatePreview + (error, properties, internalProperties) => { + return 1; + } +); + +function render() { + return ( + + + this.setState({ + progress: Math.round( + 100 * e.nativeEvent.loaded / e.nativeEvent.total + ) + }) + } + /> + + ); +} + +function render() { + return ( + + + this.setState({ + progress: Math.round( + 100 * e.nativeEvent.loaded / e.nativeEvent.total + ) + }) + } + /> + + ); +} + +function render() { + return ( + + + this.setState({ + progress: Math.round( + 100 * e.nativeEvent.loaded / e.nativeEvent.total + ) + }) + } + /> + + ); +} + +jest.mock( + "../SearchSource", + () => + class { + findMatchingTests(pattern) { + return { paths: [] }; + } + } +); + +fooooooooooooooooooooooooooooooooooooooooooooooooooo(action => next => + dispatch(action) +); + +`; + exports[`comment.js 1`] = ` /** * Curried function that ends with a BEM CSS Selector @@ -313,11 +577,82 @@ export const bem = block => `; +exports[`comment.js 2`] = ` +/** + * Curried function that ends with a BEM CSS Selector + * + * @param {String} block - the BEM Block you'd like to select. + * @returns {Function} + */ +export const bem = block => + /** + * @param {String} [element] - the BEM Element within that block; if undefined, selects the block itself. + * @returns {Function} + */ + element => + /** + * @param {?String} [modifier] - the BEM Modifier for the Block or Element; if undefined, selects the Block or Element unmodified. + * @returns {String} + */ + modifier => + [ + ".", + css(block), + element ? \`__\${css(element)}\` : "", + modifier ? \`--\${css(modifier)}\` : "" + ].join(""); + + {info.item.widget.missingProp}} + data={data} +/> +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +/** + * Curried function that ends with a BEM CSS Selector + * + * @param {String} block - the BEM Block you'd like to select. + * @returns {Function} + */ +export const bem = block => + /** + * @param {String} [element] - the BEM Element within that block; if undefined, selects the block itself. + * @returns {Function} + */ + element => + /** + * @param {?String} [modifier] - the BEM Modifier for the Block or Element; if undefined, selects the Block or Element unmodified. + * @returns {String} + */ + modifier => + [ + ".", + css(block), + element ? \`__\${css(element)}\` : "", + modifier ? \`--\${css(modifier)}\` : "" + ].join(""); + + {info.item.widget.missingProp}} + data={data} +/>; + +`; + exports[`currying.js 1`] = ` const fn = b => c => d => { return 3; }; +const foo = (a, b) => c => d => { + return 3; +}; + +const bar = a => b => c => a + b + c + const mw = store => next => action => { return next(action) } @@ -330,6 +665,51 @@ const fn = b => c => d => { return 3; }; +const foo = (a, b) => c => d => { + return 3; +}; + +const bar = a => b => c => a + b + c; + +const mw = store => next => action => { + return next(action); +}; + +const middleware = options => (req, res, next) => { + // ... +}; + +`; + +exports[`currying.js 2`] = ` +const fn = b => c => d => { + return 3; +}; + +const foo = (a, b) => c => d => { + return 3; +}; + +const bar = a => b => c => a + b + c + +const mw = store => next => action => { + return next(action) +} + +const middleware = options => (req, res, next) => { + // ... +}; +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +const fn = b => c => d => { + return 3; +}; + +const foo = (a, b) => c => d => { + return 3; +}; + +const bar = a => b => c => a + b + c; + const mw = store => next => action => { return next(action); }; @@ -350,6 +730,16 @@ veryLongCall( `; +exports[`long-call-no-args.js 2`] = ` +veryLongCall(VERY_VERY_VERY_VERY_VERY_VERY_VERY_VERY_VERY_VERY_LONG_CONSTANT, () => {}) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +veryLongCall( + VERY_VERY_VERY_VERY_VERY_VERY_VERY_VERY_VERY_VERY_LONG_CONSTANT, + () => {} +); + +`; + exports[`long-contents.js 1`] = ` const foo = () => { expect(arg1, arg2, arg3).toEqual({message: 'test', messageType: 'SMS', status: 'Unknown', created: '11/01/2017 13:36'}); @@ -366,6 +756,102 @@ const foo = () => { `; +exports[`long-contents.js 2`] = ` +const foo = () => { + expect(arg1, arg2, arg3).toEqual({message: 'test', messageType: 'SMS', status: 'Unknown', created: '11/01/2017 13:36'}); +}; +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +const foo = () => { + expect(arg1, arg2, arg3).toEqual({ + message: "test", + messageType: "SMS", + status: "Unknown", + created: "11/01/2017 13:36" + }); +}; + +`; + +exports[`parens.js 1`] = ` +promise.then( + (result) => result, + (err) => err +) + +promise.then( + (result) => { f(); return result }, + (err) => { f(); return err } +) + +foo(a => b) +foo(a => { return b }) +foo(c, a => b) +foo(c, a => b, d) +foo(a => b, d) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +promise.then(result => result, err => err); + +promise.then( + result => { + f(); + return result; + }, + err => { + f(); + return err; + } +); + +foo(a => b); +foo(a => { + return b; +}); +foo(c, a => b); +foo(c, a => b, d); +foo(a => b, d); + +`; + +exports[`parens.js 2`] = ` +promise.then( + (result) => result, + (err) => err +) + +promise.then( + (result) => { f(); return result }, + (err) => { f(); return err } +) + +foo(a => b) +foo(a => { return b }) +foo(c, a => b) +foo(c, a => b, d) +foo(a => b, d) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +promise.then(result => result, err => err); + +promise.then( + result => { + f(); + return result; + }, + err => { + f(); + return err; + } +); + +foo(a => b); +foo(a => { + return b; +}); +foo(c, a => b); +foo(c, a => b, d); +foo(a => b, d); + +`; + exports[`short_body.js 1`] = ` const initializeSnapshotState = ( testFile: Path, @@ -383,9 +869,33 @@ const initializeSnapshotState = ( `; +exports[`short_body.js 2`] = ` +const initializeSnapshotState = ( + testFile: Path, + update: boolean, + testPath: string, + expand: boolean, +) => new SnapshotState(testFile, update, testPath, expand); +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +const initializeSnapshotState = ( + testFile: Path, + update: boolean, + testPath: string, + expand: boolean +) => new SnapshotState(testFile, update, testPath, expand); + +`; + exports[`type_params.js 1`] = ` (a) => { } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ (a) => {}; `; + +exports[`type_params.js 2`] = ` +(a) => { } +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +(a) => {}; + +`; diff --git a/tests/arrows/arrow_function_expression.js b/tests/arrows/arrow_function_expression.js index 4e0071e03..ceae6d7ae 100644 --- a/tests/arrows/arrow_function_expression.js +++ b/tests/arrows/arrow_function_expression.js @@ -29,3 +29,7 @@ x => (y = z); x => (y += z); f(a => ({})) + 1; (a => ({})) || 0; +a = b => c; +a = b => { + return c +}; diff --git a/tests/arrows/currying.js b/tests/arrows/currying.js index e1a4c7e2a..1e3b36d1f 100644 --- a/tests/arrows/currying.js +++ b/tests/arrows/currying.js @@ -2,6 +2,12 @@ const fn = b => c => d => { return 3; }; +const foo = (a, b) => c => d => { + return 3; +}; + +const bar = a => b => c => a + b + c + const mw = store => next => action => { return next(action) } diff --git a/tests/arrows/jsfmt.spec.js b/tests/arrows/jsfmt.spec.js index 6a6711aef..5db62db64 100644 --- a/tests/arrows/jsfmt.spec.js +++ b/tests/arrows/jsfmt.spec.js @@ -1 +1,6 @@ -run_spec(__dirname, { parser: "babylon" }, ["typescript"]); +run_spec(__dirname, { parser: "babylon", arrowFunctionParentheses: "avoid" }, [ + "typescript" +]); +run_spec(__dirname, { parser: "babylon", arrowFunctionParentheses: "always" }, [ + "typescript" +]); diff --git a/tests/arrows/parens.js b/tests/arrows/parens.js new file mode 100644 index 000000000..e9b5f424c --- /dev/null +++ b/tests/arrows/parens.js @@ -0,0 +1,15 @@ +promise.then( + (result) => result, + (err) => err +) + +promise.then( + (result) => { f(); return result }, + (err) => { f(); return err } +) + +foo(a => b) +foo(a => { return b }) +foo(c, a => b) +foo(c, a => b, d) +foo(a => b, d) diff --git a/tests_integration/__tests__/__snapshots__/early-exit.js.snap b/tests_integration/__tests__/__snapshots__/early-exit.js.snap index 681c3baed..35aced0cd 100644 --- a/tests_integration/__tests__/__snapshots__/early-exit.js.snap +++ b/tests_integration/__tests__/__snapshots__/early-exit.js.snap @@ -1,5 +1,23 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`show detailed usage with --help arrow-function-parentheses (stderr) 1`] = `""`; + +exports[`show detailed usage with --help arrow-function-parentheses (stdout) 1`] = ` +"--arrow-function-parentheses + + Include parentheses around a sole arrow function parameter. + +Valid options: + + avoid Omit parens when possible. Example: \`x => x\` + always Always include parens. Example: \`(x) => x\` + +Default: avoid +" +`; + +exports[`show detailed usage with --help arrow-function-parentheses (write) 1`] = `Array []`; + exports[`show detailed usage with --help bracket-spacing (stderr) 1`] = `""`; exports[`show detailed usage with --help bracket-spacing (stdout) 1`] = ` @@ -477,6 +495,9 @@ Output options: Format options: + --arrow-function-parentheses + Include parentheses around a sole arrow function parameter. + Defaults to avoid. --no-bracket-spacing Do not print spaces between brackets. --jsx-bracket-same-line Put > on the last line instead of at a new line. Defaults to false. @@ -610,6 +631,9 @@ Output options: Format options: + --arrow-function-parentheses + Include parentheses around a sole arrow function parameter. + Defaults to avoid. --no-bracket-spacing Do not print spaces between brackets. --jsx-bracket-same-line Put > on the last line instead of at a new line. Defaults to false.