diff --git a/docs/rules/object-curly-newline.md b/docs/rules/object-curly-newline.md index a4907046f03..61c0e6862d9 100644 --- a/docs/rules/object-curly-newline.md +++ b/docs/rules/object-curly-newline.md @@ -19,19 +19,23 @@ Or an object option: * `"minProperties"` requires line breaks if the number of properties is at least the given integer. By default, an error will also be reported if an object contains linebreaks and has fewer properties than the given integer. However, the second behavior is disabled if the `consistent` option is set to `true` * `"consistent": true` requires that either both curly braces, or neither, directly enclose newlines. Note that enabling this option will also change the behavior of the `minProperties` option. (See `minProperties` above for more information) -You can specify different options for object literals and destructuring assignments: +You can specify different options for object literals, destructuring assignments, and named imports and exports: ```json { "object-curly-newline": ["error", { "ObjectExpression": "always", - "ObjectPattern": { "multiline": true } + "ObjectPattern": { "multiline": true }, + "ImportDeclaration": "never", + "ExportDeclaration": { "multiline": true, "minProperties": 3 } }] } ``` * `"ObjectExpression"` configuration for object literals * `"ObjectPattern"` configuration for object patterns of destructuring assignments +* `"ImportDeclaration"` configuration for named imports +* `"ExportDeclaration"` configuration for named exports ### always @@ -463,6 +467,48 @@ let {k = function() { }} = obj; ``` +### ImportDeclaration and ExportDeclaration + +Examples of **incorrect** code for this rule with the `{ "ImportDeclaration": "always", "ExportDeclaration": "never" }` options: + +```js +/*eslint object-curly-newline: ["error", { "ImportDeclaration": "always", "ExportDeclaration": "never" }]*/ +/*eslint-env es6*/ + +import {foo, bar} from 'foo-bar'; +import {foo as f, bar} from 'foo-bar'; +import {foo, + bar} from 'foo-bar'; + +export { + foo, + bar +}; +export { + foo as f, + bar +} from 'foo-bar'; +``` + +Examples of **correct** code for this rule with the `{ "ImportDeclaration": "always", "ExportDeclaration": "never" }` options: + +```js +/*eslint object-curly-newline: ["error", { "ImportDeclaration": "always", "ExportDeclaration": "never" }]*/ +/*eslint-env es6*/ + +import { + foo, + bar +} from 'foo-bar'; +import { + foo as f, + bar +} from 'foo-bar'; + +export { foo, bar } from 'foo-bar'; +export { foo as f, bar } from 'foo-bar'; +``` + ## Compatibility * **JSCS**: [requirePaddingNewLinesInObjects](http://jscs.info/rule/requirePaddingNewLinesInObjects) and [disallowPaddingNewLinesInObjects](http://jscs.info/rule/disallowPaddingNewLinesInObjects) diff --git a/lib/rules/object-curly-newline.js b/lib/rules/object-curly-newline.js index 91b2ca6c97c..14b5d06a723 100644 --- a/lib/rules/object-curly-newline.js +++ b/lib/rules/object-curly-newline.js @@ -10,6 +10,7 @@ //------------------------------------------------------------------------------ const astUtils = require("../ast-utils"); +const lodash = require("lodash"); //------------------------------------------------------------------------------ // Helpers @@ -73,19 +74,57 @@ function normalizeOptionValue(value) { * Normalizes a given option value. * * @param {string|Object|undefined} options - An option value to parse. - * @returns {{ObjectExpression: {multiline: boolean, minProperties: number}, ObjectPattern: {multiline: boolean, minProperties: number}}} Normalized option object. + * @returns {{ + * ObjectExpression: {multiline: boolean, minProperties: number, consistent: boolean}, + * ObjectPattern: {multiline: boolean, minProperties: number, consistent: boolean}, + * ImportDeclaration: {multiline: boolean, minProperties: number, consistent: boolean}, + * ExportNamedDeclaration : {multiline: boolean, minProperties: number, consistent: boolean} + * }} Normalized option object. */ function normalizeOptions(options) { - if (options && (options.ObjectExpression || options.ObjectPattern)) { + const isNodeSpecificOption = lodash.overSome([lodash.isPlainObject, lodash.isString]); + + if (lodash.isPlainObject(options) && lodash.some(options, isNodeSpecificOption)) { return { ObjectExpression: normalizeOptionValue(options.ObjectExpression), - ObjectPattern: normalizeOptionValue(options.ObjectPattern) + ObjectPattern: normalizeOptionValue(options.ObjectPattern), + ImportDeclaration: normalizeOptionValue(options.ImportDeclaration), + ExportNamedDeclaration: normalizeOptionValue(options.ExportDeclaration) }; } const value = normalizeOptionValue(options); - return { ObjectExpression: value, ObjectPattern: value }; + return { ObjectExpression: value, ObjectPattern: value, ImportDeclaration: value, ExportNamedDeclaration: value }; +} + +/** + * Determines if ObjectExpression, ObjectPattern, ImportDeclaration or ExportNamedDeclaration + * node needs to be checked for missing line breaks + * + * @param {ASTNode} node - Node under inspection + * @param {Object} options - option specific to node type + * @param {Token} first - First object property + * @param {Token} last - Last object property + * @returns {boolean} `true` if node needs to be checked for missing line breaks + */ +function areLineBreaksRequired(node, options, first, last) { + let objectProperties; + + if (node.type === "ObjectExpression" || node.type === "ObjectPattern") { + objectProperties = node.properties; + } else { + + // is ImportDeclaration or ExportNamedDeclaration + objectProperties = node.specifiers; + } + + return objectProperties.length >= options.minProperties || + ( + options.multiline && + objectProperties.length > 0 && + first.loc.start.line !== last.loc.end.line + ); } //------------------------------------------------------------------------------ @@ -109,7 +148,9 @@ module.exports = { type: "object", properties: { ObjectExpression: OPTION_VALUE, - ObjectPattern: OPTION_VALUE + ObjectPattern: OPTION_VALUE, + ImportDeclaration: OPTION_VALUE, + ExportDeclaration: OPTION_VALUE }, additionalProperties: false, minProperties: 1 @@ -125,32 +166,37 @@ module.exports = { /** * Reports a given node if it violated this rule. - * - * @param {ASTNode} node - A node to check. This is an ObjectExpression node or an ObjectPattern node. - * @param {{multiline: boolean, minProperties: number}} options - An option object. + * @param {ASTNode} node - A node to check. This is an ObjectExpression, ObjectPattern, ImportDeclaration or ExportNamedDeclaration node. + * @param {{multiline: boolean, minProperties: number, consistent: boolean}} options - An option object. * @returns {void} */ function check(node) { const options = normalizedOptions[node.type]; + + if ( + (node.type === "ImportDeclaration" && + !node.specifiers.some(specifier => specifier.type === "ImportSpecifier")) || + (node.type === "ExportNamedDeclaration" && + !node.specifiers.some(specifier => specifier.type === "ExportSpecifier")) + ) { + return; + } + const openBrace = sourceCode.getFirstToken(node, token => token.value === "{"); + let closeBrace; if (node.typeAnnotation) { closeBrace = sourceCode.getTokenBefore(node.typeAnnotation); } else { - closeBrace = sourceCode.getLastToken(node); + closeBrace = sourceCode.getLastToken(node, token => token.value === "}"); } let first = sourceCode.getTokenAfter(openBrace, { includeComments: true }); let last = sourceCode.getTokenBefore(closeBrace, { includeComments: true }); - const needsLinebreaks = ( - node.properties.length >= options.minProperties || - ( - options.multiline && - node.properties.length > 0 && - first.loc.start.line !== last.loc.end.line - ) - ); + + const needsLineBreaks = areLineBreaksRequired(node, options, first, last); + const hasCommentsFirstToken = astUtils.isCommentToken(first); const hasCommentsLastToken = astUtils.isCommentToken(last); @@ -165,7 +211,7 @@ module.exports = { first = sourceCode.getTokenAfter(openBrace); last = sourceCode.getTokenBefore(closeBrace); - if (needsLinebreaks) { + if (needsLineBreaks) { if (astUtils.isTokenOnSameLine(openBrace, first)) { context.report({ message: "Expected a line break after this opening brace.", @@ -244,7 +290,9 @@ module.exports = { return { ObjectExpression: check, - ObjectPattern: check + ObjectPattern: check, + ImportDeclaration: check, + ExportNamedDeclaration: check }; } }; diff --git a/tests/lib/rules/object-curly-newline.js b/tests/lib/rules/object-curly-newline.js index cdbf75750f7..73d9a233a94 100644 --- a/tests/lib/rules/object-curly-newline.js +++ b/tests/lib/rules/object-curly-newline.js @@ -17,7 +17,7 @@ const resolvePath = require("path").resolve, // Tests //------------------------------------------------------------------------------ -const ruleTester = new RuleTester(); +const ruleTester = new RuleTester({ parserOptions: { sourceType: "module" } }); ruleTester.run("object-curly-newline", rule, { valid: [ @@ -432,6 +432,119 @@ ruleTester.run("object-curly-newline", rule, { ].join("\n"), options: [{ ObjectExpression: "always", ObjectPattern: "never" }], parserOptions: { ecmaVersion: 6 } + }, + + // "ImportDeclaration" --------------------------------------------- + { + code: [ + "import {", + " a,", + " b", + "} from 'module';" + ].join("\n"), + options: [{ ImportDeclaration: "always" }] + }, + { + code: [ + "import {a as a, b} from 'module';" + ].join("\n"), + options: [{ ImportDeclaration: "never" }] + }, + { + code: [ + "import { a, } from 'module';" + ].join("\n"), + options: [{ ImportDeclaration: { multiline: true } }] + }, + { + code: [ + "import {", + "a, ", + "b", + "} from 'module';" + ].join("\n"), + options: [{ ImportDeclaration: { multiline: true } }] + }, + { + code: [ + "import {", + " a,", + "} from 'module';" + ].join("\n"), + options: [{ ImportDeclaration: { consistent: true } }] + }, + { + code: [ + "import { a } from 'module';" + ].join("\n"), + options: [{ ImportDeclaration: { consistent: true } }] + }, + { + code: [ + "import {", + "a, b", + "} from 'module';" + ].join("\n"), + options: [{ ImportDeclaration: { minProperties: 2 } }] + }, + { + code: [ + "import {a, b} from 'module';" + ].join("\n"), + options: [{ ImportDeclaration: { minProperties: 3 } }] + }, + + // "ExportDeclaration" --------------------------------------------- + { + code: [ + "export {a,", + "b};" + ].join("\n"), + options: [{ ExportDeclaration: "never" }] + }, + { + code: [ + "export {", + "a as a, b", + "} from 'module';" + ].join("\n"), + options: [{ ExportDeclaration: "always" }] + }, + { + code: [ + "export { a } from 'module';" + ].join("\n"), + options: [{ ExportDeclaration: { multiline: true } }] + }, + { + code: [ + "export {", + "a, ", + "b", + "} from 'module';" + ].join("\n"), + options: [{ ExportDeclaration: { multiline: true } }] + }, + { + code: [ + "export {a, ", + "b} from 'module';" + ].join("\n"), + options: [{ ExportDeclaration: { consistent: true } }] + }, + { + code: [ + "export {", + "a, b", + "} from 'module';" + ].join("\n"), + options: [{ ExportDeclaration: { minProperties: 2 } }] + }, + { + code: [ + "export {a, b} from 'module';" + ].join("\n"), + options: [{ ExportDeclaration: { minProperties: 3 } }] } ], invalid: [ @@ -1352,6 +1465,239 @@ ruleTester.run("object-curly-newline", rule, { { line: 3, column: 5, message: "Expected a line break after this opening brace." }, { line: 3, column: 16, message: "Expected a line break before this closing brace." } ] + }, + + // "ImportDeclaration" --------------------------------------------- + { + code: [ + "import {", + " a,", + " b", + "} from 'module';" + ].join("\n"), + output: [ + "import {a,", + " b} from 'module';" + ].join("\n"), + options: [{ ImportDeclaration: "never" }], + errors: [ + { line: 1, column: 8, message: "Unexpected line break after this opening brace." }, + { line: 4, column: 1, message: "Unexpected line break before this closing brace." } + ] + }, + { + code: [ + "import {a, b} from 'module';" + ].join("\n"), + output: [ + "import {", + "a, b", + "} from 'module';" + ].join("\n"), + options: [{ ImportDeclaration: "always" }], + errors: [ + { line: 1, column: 8, message: "Expected a line break after this opening brace." }, + { line: 1, column: 13, message: "Expected a line break before this closing brace." } + ] + }, + { + code: [ + "import {a as c, b} from 'module';" + ].join("\n"), + output: [ + "import {", + "a as c, b", + "} from 'module';" + ].join("\n"), + options: [{ ImportDeclaration: "always" }], + errors: [ + { line: 1, column: 8, message: "Expected a line break after this opening brace." }, + { line: 1, column: 18, message: "Expected a line break before this closing brace." } + ] + }, + { + code: [ + "import {a, ", + "b} from 'module';" + ].join("\n"), + output: [ + "import {", + "a, ", + "b", + "} from 'module';" + ].join("\n"), + options: [{ ImportDeclaration: { multiline: true } }], + errors: [ + { line: 1, column: 8, message: "Expected a line break after this opening brace." }, + { line: 2, column: 2, message: "Expected a line break before this closing brace." } + ] + }, + { + code: [ + "import {a, ", + "b", + "} from 'module';" + ].join("\n"), + output: [ + "import {a, ", + "b} from 'module';" + ].join("\n"), + options: [{ ImportDeclaration: { consistent: true } }], + errors: [ + { line: 3, column: 1, message: "Unexpected line break before this closing brace." } + ] + }, + { + code: [ + "import {a, b", + "} from 'module';" + ].join("\n"), + output: [ + "import {a, b} from 'module';" + ].join("\n"), + options: [{ ImportDeclaration: { consistent: true } }], + errors: [ + { line: 2, column: 1, message: "Unexpected line break before this closing brace." } + ] + }, + { + code: [ + "import {a, b} from 'module';" + ].join("\n"), + output: [ + "import {", + "a, b", + "} from 'module';" + ].join("\n"), + options: [{ ImportDeclaration: { minProperties: 2 } }], + errors: [ + { line: 1, column: 8, message: "Expected a line break after this opening brace." }, + { line: 1, column: 13, message: "Expected a line break before this closing brace." } + ] + }, + { + code: [ + "import {", + "a, b", + "} from 'module';" + ].join("\n"), + output: [ + "import {a, b} from 'module';" + ].join("\n"), + options: [{ ImportDeclaration: { minProperties: 3 } }], + errors: [ + { line: 1, column: 8, message: "Unexpected line break after this opening brace." }, + { line: 3, column: 1, message: "Unexpected line break before this closing brace." } + ] + }, + + // "ExportDeclaration" --------------------------------------------- + { + code: [ + "export {", + " a,", + " b", + "};" + ].join("\n"), + output: [ + "export {a,", + " b};" + ].join("\n"), + options: [{ ExportDeclaration: "never" }], + errors: [ + { line: 1, column: 8, message: "Unexpected line break after this opening brace." }, + { line: 4, column: 1, message: "Unexpected line break before this closing brace." } + ] + }, + { + code: [ + "export {a as a, b} from 'module';" + ].join("\n"), + output: [ + "export {", + "a as a, b", + "} from 'module';" + ].join("\n"), + options: [{ ExportDeclaration: "always" }], + errors: [ + { line: 1, column: 8, message: "Expected a line break after this opening brace." }, + { line: 1, column: 18, message: "Expected a line break before this closing brace." } + ] + }, + { + code: [ + "export {a, ", + "b} from 'module';" + ].join("\n"), + output: [ + "export {", + "a, ", + "b", + "} from 'module';" + ].join("\n"), + options: [{ ExportDeclaration: { multiline: true } }], + errors: [ + { line: 1, column: 8, message: "Expected a line break after this opening brace." }, + { line: 2, column: 2, message: "Expected a line break before this closing brace." } + ] + }, + { + code: [ + "export {a, ", + "b,", + "} from 'module';" + ].join("\n"), + output: [ + "export {a, ", + "b,} from 'module';" + ].join("\n"), + options: [{ ExportDeclaration: { consistent: true } }], + errors: [ + { line: 3, column: 1, message: "Unexpected line break before this closing brace." } + ] + }, + { + code: [ + "export {a, b", + "} from 'module';" + ].join("\n"), + output: [ + "export {a, b} from 'module';" + ].join("\n"), + options: [{ ExportDeclaration: { consistent: true } }], + errors: [ + { line: 2, column: 1, message: "Unexpected line break before this closing brace." } + ] + }, + { + code: [ + "export {a, b,} from 'module';" + ].join("\n"), + output: [ + "export {", + "a, b,", + "} from 'module';" + ].join("\n"), + options: [{ ExportDeclaration: { minProperties: 2 } }], + errors: [ + { line: 1, column: 8, message: "Expected a line break after this opening brace." }, + { line: 1, column: 14, message: "Expected a line break before this closing brace." } + ] + }, + { + code: [ + "export {", + "a, b", + "} from 'module';" + ].join("\n"), + output: [ + "export {a, b} from 'module';" + ].join("\n"), + options: [{ ExportDeclaration: { minProperties: 3 } }], + errors: [ + { line: 1, column: 8, message: "Unexpected line break after this opening brace." }, + { line: 3, column: 1, message: "Unexpected line break before this closing brace." } + ] } ] });