You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

306 lines
11 KiB

4 years ago
  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. messages: {
  143. unexpectedLinebreakBeforeClosingBrace: "Unexpected line break before this closing brace.",
  144. unexpectedLinebreakAfterOpeningBrace: "Unexpected line break after this opening brace.",
  145. expectedLinebreakBeforeClosingBrace: "Expected a line break before this closing brace.",
  146. expectedLinebreakAfterOpeningBrace: "Expected a line break after this opening brace."
  147. }
  148. },
  149. create(context) {
  150. const sourceCode = context.getSourceCode();
  151. const normalizedOptions = normalizeOptions(context.options[0]);
  152. /**
  153. * Reports a given node if it violated this rule.
  154. * @param {ASTNode} node A node to check. This is an ObjectExpression, ObjectPattern, ImportDeclaration or ExportNamedDeclaration node.
  155. * @returns {void}
  156. */
  157. function check(node) {
  158. const options = normalizedOptions[node.type];
  159. if (
  160. (node.type === "ImportDeclaration" &&
  161. !node.specifiers.some(specifier => specifier.type === "ImportSpecifier")) ||
  162. (node.type === "ExportNamedDeclaration" &&
  163. !node.specifiers.some(specifier => specifier.type === "ExportSpecifier"))
  164. ) {
  165. return;
  166. }
  167. const openBrace = sourceCode.getFirstToken(node, token => token.value === "{");
  168. let closeBrace;
  169. if (node.typeAnnotation) {
  170. closeBrace = sourceCode.getTokenBefore(node.typeAnnotation);
  171. } else {
  172. closeBrace = sourceCode.getLastToken(node, token => token.value === "}");
  173. }
  174. let first = sourceCode.getTokenAfter(openBrace, { includeComments: true });
  175. let last = sourceCode.getTokenBefore(closeBrace, { includeComments: true });
  176. const needsLineBreaks = areLineBreaksRequired(node, options, first, last);
  177. const hasCommentsFirstToken = astUtils.isCommentToken(first);
  178. const hasCommentsLastToken = astUtils.isCommentToken(last);
  179. /*
  180. * Use tokens or comments to check multiline or not.
  181. * But use only tokens to check whether line breaks are needed.
  182. * This allows:
  183. * var obj = { // eslint-disable-line foo
  184. * a: 1
  185. * }
  186. */
  187. first = sourceCode.getTokenAfter(openBrace);
  188. last = sourceCode.getTokenBefore(closeBrace);
  189. if (needsLineBreaks) {
  190. if (astUtils.isTokenOnSameLine(openBrace, first)) {
  191. context.report({
  192. messageId: "expectedLinebreakAfterOpeningBrace",
  193. node,
  194. loc: openBrace.loc,
  195. fix(fixer) {
  196. if (hasCommentsFirstToken) {
  197. return null;
  198. }
  199. return fixer.insertTextAfter(openBrace, "\n");
  200. }
  201. });
  202. }
  203. if (astUtils.isTokenOnSameLine(last, closeBrace)) {
  204. context.report({
  205. messageId: "expectedLinebreakBeforeClosingBrace",
  206. node,
  207. loc: closeBrace.loc,
  208. fix(fixer) {
  209. if (hasCommentsLastToken) {
  210. return null;
  211. }
  212. return fixer.insertTextBefore(closeBrace, "\n");
  213. }
  214. });
  215. }
  216. } else {
  217. const consistent = options.consistent;
  218. const hasLineBreakBetweenOpenBraceAndFirst = !astUtils.isTokenOnSameLine(openBrace, first);
  219. const hasLineBreakBetweenCloseBraceAndLast = !astUtils.isTokenOnSameLine(last, closeBrace);
  220. if (
  221. (!consistent && hasLineBreakBetweenOpenBraceAndFirst) ||
  222. (consistent && hasLineBreakBetweenOpenBraceAndFirst && !hasLineBreakBetweenCloseBraceAndLast)
  223. ) {
  224. context.report({
  225. messageId: "unexpectedLinebreakAfterOpeningBrace",
  226. node,
  227. loc: openBrace.loc,
  228. fix(fixer) {
  229. if (hasCommentsFirstToken) {
  230. return null;
  231. }
  232. return fixer.removeRange([
  233. openBrace.range[1],
  234. first.range[0]
  235. ]);
  236. }
  237. });
  238. }
  239. if (
  240. (!consistent && hasLineBreakBetweenCloseBraceAndLast) ||
  241. (consistent && !hasLineBreakBetweenOpenBraceAndFirst && hasLineBreakBetweenCloseBraceAndLast)
  242. ) {
  243. context.report({
  244. messageId: "unexpectedLinebreakBeforeClosingBrace",
  245. node,
  246. loc: closeBrace.loc,
  247. fix(fixer) {
  248. if (hasCommentsLastToken) {
  249. return null;
  250. }
  251. return fixer.removeRange([
  252. last.range[1],
  253. closeBrace.range[0]
  254. ]);
  255. }
  256. });
  257. }
  258. }
  259. }
  260. return {
  261. ObjectExpression: check,
  262. ObjectPattern: check,
  263. ImportDeclaration: check,
  264. ExportNamedDeclaration: check
  265. };
  266. }
  267. };