|
|
/** * @fileoverview Rule to require or disallow line breaks inside braces. * @author Toru Nagashima */
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("./utils/ast-utils"); const lodash = require("lodash");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
// Schema objects.
const OPTION_VALUE = { oneOf: [ { enum: ["always", "never"] }, { type: "object", properties: { multiline: { type: "boolean" }, minProperties: { type: "integer", minimum: 0 }, consistent: { type: "boolean" } }, additionalProperties: false, minProperties: 1 } ] };
/** * Normalizes a given option value. * @param {string|Object|undefined} value An option value to parse. * @returns {{multiline: boolean, minProperties: number, consistent: boolean}} Normalized option object. */ function normalizeOptionValue(value) { let multiline = false; let minProperties = Number.POSITIVE_INFINITY; let consistent = false;
if (value) { if (value === "always") { minProperties = 0; } else if (value === "never") { minProperties = Number.POSITIVE_INFINITY; } else { multiline = Boolean(value.multiline); minProperties = value.minProperties || Number.POSITIVE_INFINITY; consistent = Boolean(value.consistent); } } else { consistent = true; }
return { multiline, minProperties, consistent }; }
/** * Normalizes a given option value. * @param {string|Object|undefined} options An option value to parse. * @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) { 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), ImportDeclaration: normalizeOptionValue(options.ImportDeclaration), ExportNamedDeclaration: normalizeOptionValue(options.ExportDeclaration) }; }
const value = normalizeOptionValue(options);
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 .filter(s => s.type === "ImportSpecifier" || s.type === "ExportSpecifier"); }
return objectProperties.length >= options.minProperties || ( options.multiline && objectProperties.length > 0 && first.loc.start.line !== last.loc.end.line ); }
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = { meta: { type: "layout",
docs: { description: "enforce consistent line breaks inside braces", category: "Stylistic Issues", recommended: false, url: "https://eslint.org/docs/rules/object-curly-newline" },
fixable: "whitespace",
schema: [ { oneOf: [ OPTION_VALUE, { type: "object", properties: { ObjectExpression: OPTION_VALUE, ObjectPattern: OPTION_VALUE, ImportDeclaration: OPTION_VALUE, ExportDeclaration: OPTION_VALUE }, additionalProperties: false, minProperties: 1 } ] } ],
messages: { unexpectedLinebreakBeforeClosingBrace: "Unexpected line break before this closing brace.", unexpectedLinebreakAfterOpeningBrace: "Unexpected line break after this opening brace.", expectedLinebreakBeforeClosingBrace: "Expected a line break before this closing brace.", expectedLinebreakAfterOpeningBrace: "Expected a line break after this opening brace." } },
create(context) { const sourceCode = context.getSourceCode(); const normalizedOptions = normalizeOptions(context.options[0]);
/** * Reports a given node if it violated this rule. * @param {ASTNode} node A node to check. This is an ObjectExpression, ObjectPattern, ImportDeclaration or ExportNamedDeclaration node. * @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, token => token.value === "}"); }
let first = sourceCode.getTokenAfter(openBrace, { includeComments: true }); let last = sourceCode.getTokenBefore(closeBrace, { includeComments: true });
const needsLineBreaks = areLineBreaksRequired(node, options, first, last);
const hasCommentsFirstToken = astUtils.isCommentToken(first); const hasCommentsLastToken = astUtils.isCommentToken(last);
/* * Use tokens or comments to check multiline or not. * But use only tokens to check whether line breaks are needed. * This allows: * var obj = { // eslint-disable-line foo
* a: 1 * } */ first = sourceCode.getTokenAfter(openBrace); last = sourceCode.getTokenBefore(closeBrace);
if (needsLineBreaks) { if (astUtils.isTokenOnSameLine(openBrace, first)) { context.report({ messageId: "expectedLinebreakAfterOpeningBrace", node, loc: openBrace.loc, fix(fixer) { if (hasCommentsFirstToken) { return null; }
return fixer.insertTextAfter(openBrace, "\n"); } }); } if (astUtils.isTokenOnSameLine(last, closeBrace)) { context.report({ messageId: "expectedLinebreakBeforeClosingBrace", node, loc: closeBrace.loc, fix(fixer) { if (hasCommentsLastToken) { return null; }
return fixer.insertTextBefore(closeBrace, "\n"); } }); } } else { const consistent = options.consistent; const hasLineBreakBetweenOpenBraceAndFirst = !astUtils.isTokenOnSameLine(openBrace, first); const hasLineBreakBetweenCloseBraceAndLast = !astUtils.isTokenOnSameLine(last, closeBrace);
if ( (!consistent && hasLineBreakBetweenOpenBraceAndFirst) || (consistent && hasLineBreakBetweenOpenBraceAndFirst && !hasLineBreakBetweenCloseBraceAndLast) ) { context.report({ messageId: "unexpectedLinebreakAfterOpeningBrace", node, loc: openBrace.loc, fix(fixer) { if (hasCommentsFirstToken) { return null; }
return fixer.removeRange([ openBrace.range[1], first.range[0] ]); } }); } if ( (!consistent && hasLineBreakBetweenCloseBraceAndLast) || (consistent && !hasLineBreakBetweenOpenBraceAndFirst && hasLineBreakBetweenCloseBraceAndLast) ) { context.report({ messageId: "unexpectedLinebreakBeforeClosingBrace", node, loc: closeBrace.loc, fix(fixer) { if (hasCommentsLastToken) { return null; }
return fixer.removeRange([ last.range[1], closeBrace.range[0] ]); } }); } } }
return { ObjectExpression: check, ObjectPattern: check, ImportDeclaration: check, ExportNamedDeclaration: check }; } };
|