123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321 |
- /**
- * @fileoverview Rule to require or disallow line breaks inside braces.
- * @author Toru Nagashima
- */
- "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('../shared/types').Rule} */
- module.exports = {
- meta: {
- type: "layout",
- docs: {
- description: "Enforce consistent line breaks after opening and before closing braces",
- 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
- };
- }
- };
|