object-curly-newline.js 11 KB

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