/** * @fileoverview Rule to require or disallow line breaks inside braces. * @author Toru Nagashima * @deprecated in ESLint v8.53.0 */ 'use strict'; //------------------------------------------------------------------------------ // Requirements //------------------------------------------------------------------------------ const astUtils = require('./utils/ast-utils'); //------------------------------------------------------------------------------ // 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 }; } /** * Checks if a value is an object. * @param {any} value The value to check * @returns {boolean} `true` if the value is an object, otherwise `false` */ function isObject(value) { return typeof value === 'object' && value !== null; } /** * Checks if an option is a node-specific option * @param {any} option The option to check * @returns {boolean} `true` if the option is node-specific, otherwise `false` */ function isNodeSpecificOption(option) { return isObject(option) || typeof option === 'string'; } /** * 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) { if (isObject(options) && Object.values(options).some(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 //------------------------------------------------------------------------------ /** @type {import('../types').Rule.RuleModule} */ module.exports = { meta: { deprecated: { message: 'Formatting rules are being moved out of ESLint core.', url: 'https://eslint.org/blog/2023/10/deprecating-formatting-rules/', deprecatedSince: '8.53.0', availableUntil: '10.0.0', replacedBy: [ { message: 'ESLint Stylistic now maintains deprecated stylistic core rules.', url: 'https://eslint.style/guide/migration', plugin: { name: '@stylistic/eslint-plugin-js', url: 'https://eslint.style/packages/js', }, rule: { name: 'object-curly-newline', url: 'https://eslint.style/rules/js/object-curly-newline', }, }, ], }, type: 'layout', docs: { description: 'Enforce consistent line breaks after opening and before closing braces', recommended: false, url: 'https://eslint.org/docs/latest/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.sourceCode; 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, }; }, };