From 696a63ae15932a45fbd0562f10eef0842f309aba Mon Sep 17 00:00:00 2001 From: Jim Jenkins Date: Fri, 28 Dec 2018 11:36:17 -0500 Subject: [PATCH] Add fork of brace-style rule to support one line arrow functions The base brace-style rule in eslint does not have an option to support one line arrow functions like: ``` myFunction((b) => { doSomething(b); }); ``` The only option available is `allowSingleLine`, which would allow the above code to pass, but would also allow one line if statements. This commit forks the brace-style rule and adds the option `allowSingleLineArrowFunctions`. --- lib/index.js | 2 + lib/rules/brace-style.js | 193 ++++++++++++++++++++++++++++ tests/lib/rules/brace-style.test.js | 96 ++++++++++++++ 3 files changed, 291 insertions(+) create mode 100644 lib/rules/brace-style.js create mode 100644 tests/lib/rules/brace-style.test.js diff --git a/lib/index.js b/lib/index.js index 35c9f26..40a3d6e 100644 --- a/lib/index.js +++ b/lib/index.js @@ -16,6 +16,7 @@ var fluentChaining = require('./rules/fluent-chaining'), noArrowProperties = require('./rules/no-arrow-for-class-property'), modulesOnly = require('./rules/module-files-only'), blockScopeCase = require('./rules/block-scope-case'), + braceStyle = require('./rules/brace-style'), indent = require('./rules/indent'); module.exports.rules = { @@ -24,6 +25,7 @@ module.exports.rules = { 'call-indentation': callIndentation, 'no-multiple-inline-functions': noMultipleInlineFunctions, 'no-multiline-var-declarations': noMultilineVarDeclaration, + 'brace-style': braceStyle, 'indent': indent, 'no-multiline-conditionals': noMultilineConditionals, 'empty-object-spacing': emptyObjectSpacing, diff --git a/lib/rules/brace-style.js b/lib/rules/brace-style.js new file mode 100644 index 0000000..17c3742 --- /dev/null +++ b/lib/rules/brace-style.js @@ -0,0 +1,193 @@ +/** + * @fileoverview Rule to flag block statements that do not use the one true brace style + * @author Ian Christian Myers + */ + +'use strict'; + +const astUtils = require('eslint/lib/util/ast-utils'); + +module.exports = { + meta: { + type: 'layout', + + docs: { + description: 'enforce consistent brace style for blocks', + category: 'Stylistic Issues', + recommended: false, + url: 'https://eslint.org/docs/rules/brace-style', + }, + + schema: [ + { + 'enum': [ '1tbs', 'stroustrup', 'allman' ], + }, + { + type: 'object', + properties: { + allowSingleLine: { + type: 'boolean', + }, + allowSingleLineArrow: { + type: 'boolean', + }, + }, + additionalProperties: false, + }, + ], + + fixable: 'whitespace', + + messages: { + nextLineOpen: 'Opening curly brace does not appear on the same line as controlling statement.', + sameLineOpen: 'Opening curly brace appears on the same line as controlling statement.', + blockSameLine: 'Statement inside of curly braces should be on next line.', + nextLineClose: 'Closing curly brace does not appear on the same line as the subsequent block.', + singleLineClose: 'Closing curly brace should be on the same line as opening curly brace or on the line after the previous block.', + sameLineClose: 'Closing curly brace appears on the same line as the subsequent block.', + }, + }, + + create(context) { + const style = context.options[0] || '1tbs', + params = context.options[1] || {}, + sourceCode = context.getSourceCode(); + + // -------------------------------------------------------------------------- + // Helpers + // -------------------------------------------------------------------------- + + /** + * Fixes a place where a newline unexpectedly appears + * @param {Token} firstToken The token before the unexpected newline + * @param {Token} secondToken The token after the unexpected newline + * @returns {Function} A fixer function to remove the newlines between the tokens + */ + function removeNewlineBetween(firstToken, secondToken) { + const textRange = [ firstToken.range[1], secondToken.range[0] ], + textBetween = sourceCode.text.slice(textRange[0], textRange[1]); + + // Don't do a fix if there is a comment between the tokens + if (textBetween.trim()) { + return null; + } + return (fixer) => fixer.replaceTextRange(textRange, ' '); + } + + /** + * Validates a pair of curly brackets based on the user's config + * @param {Token} openingCurly The opening curly bracket + * @param {Token} closingCurly The closing curly bracket + * @returns {void} + */ + function validateCurlyPair(openingCurly, closingCurly) { + const tokenBeforeOpeningCurly = sourceCode.getTokenBefore(openingCurly), + tokenAfterOpeningCurly = sourceCode.getTokenAfter(openingCurly), + tokenBeforeClosingCurly = sourceCode.getTokenBefore(closingCurly), + singleLineException = params.allowSingleLine && astUtils.isTokenOnSameLine(openingCurly, closingCurly), + isOpeningCurlyOnSameLine = astUtils.isTokenOnSameLine(openingCurly, tokenAfterOpeningCurly), + isClosingCurlyOnSameLine = astUtils.isTokenOnSameLine(tokenBeforeClosingCurly, closingCurly); + + if (style !== 'allman' && !astUtils.isTokenOnSameLine(tokenBeforeOpeningCurly, openingCurly)) { + context.report({ + node: openingCurly, + messageId: 'nextLineOpen', + fix: removeNewlineBetween(tokenBeforeOpeningCurly, openingCurly), + }); + } + + if (style === 'allman' && astUtils.isTokenOnSameLine(tokenBeforeOpeningCurly, openingCurly) && !singleLineException) { + context.report({ + node: openingCurly, + messageId: 'sameLineOpen', + fix: (fixer) => fixer.insertTextBefore(openingCurly, '\n'), + }); + } + + + if (isOpeningCurlyOnSameLine && tokenAfterOpeningCurly !== closingCurly && !singleLineException) { + context.report({ + node: openingCurly, + messageId: 'blockSameLine', + fix: (fixer) => fixer.insertTextAfter(openingCurly, '\n'), + }); + } + + if (tokenBeforeClosingCurly !== openingCurly && !singleLineException && isClosingCurlyOnSameLine) { + context.report({ + node: closingCurly, + messageId: 'singleLineClose', + fix: (fixer) => fixer.insertTextBefore(closingCurly, '\n'), + }); + } + } + + /** + * Validates the location of a token that appears before a keyword (e.g. a newline + * before `else`) + * @param {Token} curlyToken The closing curly token. This is assumed to precede a + * keyword token (such as `else` or `finally`). + * @returns {void} + */ + function validateCurlyBeforeKeyword(curlyToken) { + const keywordToken = sourceCode.getTokenAfter(curlyToken); + + if (style === '1tbs' && !astUtils.isTokenOnSameLine(curlyToken, keywordToken)) { + context.report({ + node: curlyToken, + messageId: 'nextLineClose', + fix: removeNewlineBetween(curlyToken, keywordToken), + }); + } + + if (style !== '1tbs' && astUtils.isTokenOnSameLine(curlyToken, keywordToken)) { + context.report({ + node: curlyToken, + messageId: 'sameLineClose', + fix: (fixer) => fixer.insertTextAfter(curlyToken, '\n'), + }); + } + } + + return { + BlockStatement(node) { + const isOneLine = astUtils.isTokenOnSameLine(sourceCode.getFirstToken(node), sourceCode.getLastToken(node)); + + if (params.allowSingleLineArrow && node.parent.type === 'ArrowFunctionExpression' && isOneLine) { + return; + } + + if (!astUtils.STATEMENT_LIST_PARENTS.has(node.parent.type)) { + validateCurlyPair(sourceCode.getFirstToken(node), sourceCode.getLastToken(node)); + } + }, + ClassBody(node) { + validateCurlyPair(sourceCode.getFirstToken(node), sourceCode.getLastToken(node)); + }, + SwitchStatement(node) { + const closingCurly = sourceCode.getLastToken(node), + openingCurly = sourceCode.getTokenBefore(node.cases.length ? node.cases[0] : closingCurly); + + validateCurlyPair(openingCurly, closingCurly); + }, + IfStatement(node) { + if (node.consequent.type === 'BlockStatement' && node.alternate) { + + // Handle the keyword after the `if` block (before `else`) + validateCurlyBeforeKeyword(sourceCode.getLastToken(node.consequent)); + } + }, + TryStatement(node) { + + // Handle the keyword after the `try` block (before `catch` or `finally`) + validateCurlyBeforeKeyword(sourceCode.getLastToken(node.block)); + + if (node.handler && node.finalizer) { + + // Handle the keyword after the `catch` block (before `finally`) + validateCurlyBeforeKeyword(sourceCode.getLastToken(node.handler.body)); + } + }, + }; + }, +}; diff --git a/tests/lib/rules/brace-style.test.js b/tests/lib/rules/brace-style.test.js new file mode 100644 index 0000000..e396883 --- /dev/null +++ b/tests/lib/rules/brace-style.test.js @@ -0,0 +1,96 @@ +/** + * @fileoverview Check indentation at the beginning and end of a function call + */ +'use strict'; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require('../../../lib/rules/brace-style'), + formatCode = require('../../code-helper'), + ruleTester = require('../../ruleTesters').es6(), + invalidExamples, validExample; + +invalidExamples = [ + { + code: formatCode( + 'if (a === b) { doSomething() }', + 'if (a === b) {', + ' doSomething(); }', + 'if (a === b )', + '{', + ' doSomething();', + '}', + 'let badArrow = (b) => ', + '{', + ' doSomething(b);', + '};' + ), + errors: [ + { + message: 'Statement inside of curly braces should be on next line.', + type: 'Punctuator', + }, + { + message: 'Closing curly brace should be on the same line as opening curly brace or on the line after the previous block.', + type: 'Punctuator', + }, + { + message: 'Closing curly brace should be on the same line as opening curly brace or on the line after the previous block.', + type: 'Punctuator', + }, + { + message: 'Opening curly brace does not appear on the same line as controlling statement.', + type: 'Punctuator', + }, + { + message: 'Opening curly brace does not appear on the same line as controlling statement.', + type: 'Punctuator', + }, + ], + options: [ '1tbs', { allowSingleLine: false, allowSingleLineArrow: true } ], + }, +]; + +validExample = formatCode( + 'if (a === b) {', + ' doSomething();', + '}', + 'if (a === b) {', + ' doSomething();', + '} else {', + ' doSomethingElse();', + '}', + 'while (a === b) {', + ' doSomething();', + '}', + 'function myFunc(a) {', + ' doSomething(a);', + '}', + 'let func = (a) => {', + ' doSomething(a);', + '};', + 'try {', + ' doSomething(a);', + '} catch(e) {', + ' console.log(e);', + '}', + 'myOtherFunc((a) => { doSomething(); });', + 'let func2 = (b) => { doSomething(); };' +); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +ruleTester.run('brace-style', rule, { + valid: [ + { + code: validExample, + options: [ '1tbs', { allowSingleLine: false, allowSingleLineArrow: true } ], + }, + ], + + invalid: invalidExamples, +});