object-curly-newline.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. /**
  2. * @fileoverview Rule to require or disallow line breaks inside braces.
  3. * @author Toru Nagashima
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Requirements
  8. //------------------------------------------------------------------------------
  9. const astUtils = require("./utils/ast-utils");
  10. //------------------------------------------------------------------------------
  11. // Helpers
  12. //------------------------------------------------------------------------------
  13. // Schema objects.
  14. const OPTION_VALUE = {
  15. oneOf: [
  16. {
  17. enum: ["always", "never"]
  18. },
  19. {
  20. type: "object",
  21. properties: {
  22. multiline: {
  23. type: "boolean"
  24. },
  25. minProperties: {
  26. type: "integer",
  27. minimum: 0
  28. },
  29. consistent: {
  30. type: "boolean"
  31. }
  32. },
  33. additionalProperties: false,
  34. minProperties: 1
  35. }
  36. ]
  37. };
  38. /**
  39. * Normalizes a given option value.
  40. * @param {string|Object|undefined} value An option value to parse.
  41. * @returns {{multiline: boolean, minProperties: number, consistent: boolean}} Normalized option object.
  42. */
  43. function normalizeOptionValue(value) {
  44. let multiline = false;
  45. let minProperties = Number.POSITIVE_INFINITY;
  46. let consistent = false;
  47. if (value) {
  48. if (value === "always") {
  49. minProperties = 0;
  50. } else if (value === "never") {
  51. minProperties = Number.POSITIVE_INFINITY;
  52. } else {
  53. multiline = Boolean(value.multiline);
  54. minProperties = value.minProperties || Number.POSITIVE_INFINITY;
  55. consistent = Boolean(value.consistent);
  56. }
  57. } else {
  58. consistent = true;
  59. }
  60. return { multiline, minProperties, consistent };
  61. }
  62. /**
  63. * Checks if a value is an object.
  64. * @param {any} value The value to check
  65. * @returns {boolean} `true` if the value is an object, otherwise `false`
  66. */
  67. function isObject(value) {
  68. return typeof value === "object" && value !== null;
  69. }
  70. /**
  71. * Checks if an option is a node-specific option
  72. * @param {any} option The option to check
  73. * @returns {boolean} `true` if the option is node-specific, otherwise `false`
  74. */
  75. function isNodeSpecificOption(option) {
  76. return isObject(option) || typeof option === "string";
  77. }
  78. /**
  79. * Normalizes a given option value.
  80. * @param {string|Object|undefined} options An option value to parse.
  81. * @returns {{
  82. * ObjectExpression: {multiline: boolean, minProperties: number, consistent: boolean},
  83. * ObjectPattern: {multiline: boolean, minProperties: number, consistent: boolean},
  84. * ImportDeclaration: {multiline: boolean, minProperties: number, consistent: boolean},
  85. * ExportNamedDeclaration : {multiline: boolean, minProperties: number, consistent: boolean}
  86. * }} Normalized option object.
  87. */
  88. function normalizeOptions(options) {
  89. if (isObject(options) && Object.values(options).some(isNodeSpecificOption)) {
  90. return {
  91. ObjectExpression: normalizeOptionValue(options.ObjectExpression),
  92. ObjectPattern: normalizeOptionValue(options.ObjectPattern),
  93. ImportDeclaration: normalizeOptionValue(options.ImportDeclaration),
  94. ExportNamedDeclaration: normalizeOptionValue(options.ExportDeclaration)
  95. };
  96. }
  97. const value = normalizeOptionValue(options);
  98. return { ObjectExpression: value, ObjectPattern: value, ImportDeclaration: value, ExportNamedDeclaration: value };
  99. }
  100. /**
  101. * Determines if ObjectExpression, ObjectPattern, ImportDeclaration or ExportNamedDeclaration
  102. * node needs to be checked for missing line breaks
  103. * @param {ASTNode} node Node under inspection
  104. * @param {Object} options option specific to node type
  105. * @param {Token} first First object property
  106. * @param {Token} last Last object property
  107. * @returns {boolean} `true` if node needs to be checked for missing line breaks
  108. */
  109. function areLineBreaksRequired(node, options, first, last) {
  110. let objectProperties;
  111. if (node.type === "ObjectExpression" || node.type === "ObjectPattern") {
  112. objectProperties = node.properties;
  113. } else {
  114. // is ImportDeclaration or ExportNamedDeclaration
  115. objectProperties = node.specifiers
  116. .filter(s => s.type === "ImportSpecifier" || s.type === "ExportSpecifier");
  117. }
  118. return objectProperties.length >= options.minProperties ||
  119. (
  120. options.multiline &&
  121. objectProperties.length > 0 &&
  122. first.loc.start.line !== last.loc.end.line
  123. );
  124. }
  125. //------------------------------------------------------------------------------
  126. // Rule Definition
  127. //------------------------------------------------------------------------------
  128. /** @type {import('../shared/types').Rule} */
  129. module.exports = {
  130. meta: {
  131. type: "layout",
  132. docs: {
  133. description: "Enforce consistent line breaks after opening and before closing braces",
  134. recommended: false,
  135. url: "https://eslint.org/docs/rules/object-curly-newline"
  136. },
  137. fixable: "whitespace",
  138. schema: [
  139. {
  140. oneOf: [
  141. OPTION_VALUE,
  142. {
  143. type: "object",
  144. properties: {
  145. ObjectExpression: OPTION_VALUE,
  146. ObjectPattern: OPTION_VALUE,
  147. ImportDeclaration: OPTION_VALUE,
  148. ExportDeclaration: OPTION_VALUE
  149. },
  150. additionalProperties: false,
  151. minProperties: 1
  152. }
  153. ]
  154. }
  155. ],
  156. messages: {
  157. unexpectedLinebreakBeforeClosingBrace: "Unexpected line break before this closing brace.",
  158. unexpectedLinebreakAfterOpeningBrace: "Unexpected line break after this opening brace.",
  159. expectedLinebreakBeforeClosingBrace: "Expected a line break before this closing brace.",
  160. expectedLinebreakAfterOpeningBrace: "Expected a line break after this opening brace."
  161. }
  162. },
  163. create(context) {
  164. const sourceCode = context.getSourceCode();
  165. const normalizedOptions = normalizeOptions(context.options[0]);
  166. /**
  167. * Reports a given node if it violated this rule.
  168. * @param {ASTNode} node A node to check. This is an ObjectExpression, ObjectPattern, ImportDeclaration or ExportNamedDeclaration node.
  169. * @returns {void}
  170. */
  171. function check(node) {
  172. const options = normalizedOptions[node.type];
  173. if (
  174. (node.type === "ImportDeclaration" &&
  175. !node.specifiers.some(specifier => specifier.type === "ImportSpecifier")) ||
  176. (node.type === "ExportNamedDeclaration" &&
  177. !node.specifiers.some(specifier => specifier.type === "ExportSpecifier"))
  178. ) {
  179. return;
  180. }
  181. const openBrace = sourceCode.getFirstToken(node, token => token.value === "{");
  182. let closeBrace;
  183. if (node.typeAnnotation) {
  184. closeBrace = sourceCode.getTokenBefore(node.typeAnnotation);
  185. } else {
  186. closeBrace = sourceCode.getLastToken(node, token => token.value === "}");
  187. }
  188. let first = sourceCode.getTokenAfter(openBrace, { includeComments: true });
  189. let last = sourceCode.getTokenBefore(closeBrace, { includeComments: true });
  190. const needsLineBreaks = areLineBreaksRequired(node, options, first, last);
  191. const hasCommentsFirstToken = astUtils.isCommentToken(first);
  192. const hasCommentsLastToken = astUtils.isCommentToken(last);
  193. /*
  194. * Use tokens or comments to check multiline or not.
  195. * But use only tokens to check whether line breaks are needed.
  196. * This allows:
  197. * var obj = { // eslint-disable-line foo
  198. * a: 1
  199. * }
  200. */
  201. first = sourceCode.getTokenAfter(openBrace);
  202. last = sourceCode.getTokenBefore(closeBrace);
  203. if (needsLineBreaks) {
  204. if (astUtils.isTokenOnSameLine(openBrace, first)) {
  205. context.report({
  206. messageId: "expectedLinebreakAfterOpeningBrace",
  207. node,
  208. loc: openBrace.loc,
  209. fix(fixer) {
  210. if (hasCommentsFirstToken) {
  211. return null;
  212. }
  213. return fixer.insertTextAfter(openBrace, "\n");
  214. }
  215. });
  216. }
  217. if (astUtils.isTokenOnSameLine(last, closeBrace)) {
  218. context.report({
  219. messageId: "expectedLinebreakBeforeClosingBrace",
  220. node,
  221. loc: closeBrace.loc,
  222. fix(fixer) {
  223. if (hasCommentsLastToken) {
  224. return null;
  225. }
  226. return fixer.insertTextBefore(closeBrace, "\n");
  227. }
  228. });
  229. }
  230. } else {
  231. const consistent = options.consistent;
  232. const hasLineBreakBetweenOpenBraceAndFirst = !astUtils.isTokenOnSameLine(openBrace, first);
  233. const hasLineBreakBetweenCloseBraceAndLast = !astUtils.isTokenOnSameLine(last, closeBrace);
  234. if (
  235. (!consistent && hasLineBreakBetweenOpenBraceAndFirst) ||
  236. (consistent && hasLineBreakBetweenOpenBraceAndFirst && !hasLineBreakBetweenCloseBraceAndLast)
  237. ) {
  238. context.report({
  239. messageId: "unexpectedLinebreakAfterOpeningBrace",
  240. node,
  241. loc: openBrace.loc,
  242. fix(fixer) {
  243. if (hasCommentsFirstToken) {
  244. return null;
  245. }
  246. return fixer.removeRange([
  247. openBrace.range[1],
  248. first.range[0]
  249. ]);
  250. }
  251. });
  252. }
  253. if (
  254. (!consistent && hasLineBreakBetweenCloseBraceAndLast) ||
  255. (consistent && !hasLineBreakBetweenOpenBraceAndFirst && hasLineBreakBetweenCloseBraceAndLast)
  256. ) {
  257. context.report({
  258. messageId: "unexpectedLinebreakBeforeClosingBrace",
  259. node,
  260. loc: closeBrace.loc,
  261. fix(fixer) {
  262. if (hasCommentsLastToken) {
  263. return null;
  264. }
  265. return fixer.removeRange([
  266. last.range[1],
  267. closeBrace.range[0]
  268. ]);
  269. }
  270. });
  271. }
  272. }
  273. }
  274. return {
  275. ObjectExpression: check,
  276. ObjectPattern: check,
  277. ImportDeclaration: check,
  278. ExportNamedDeclaration: check
  279. };
  280. }
  281. };