numeric-separators-style.js 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. 'use strict';
  2. const numeric = require('./utils/numeric.js');
  3. const {isBigIntLiteral} = require('./ast/index.js');
  4. const MESSAGE_ID = 'numeric-separators-style';
  5. const messages = {
  6. [MESSAGE_ID]: 'Invalid group length in numeric value.',
  7. };
  8. function addSeparator(value, {minimumDigits, groupLength}, fromLeft) {
  9. const {length} = value;
  10. if (length < minimumDigits) {
  11. return value;
  12. }
  13. const parts = [];
  14. if (fromLeft) {
  15. for (let start = 0; start < length; start += groupLength) {
  16. const end = Math.min(start + groupLength, length);
  17. parts.push(value.slice(start, end));
  18. }
  19. } else {
  20. for (let end = length; end > 0; end -= groupLength) {
  21. const start = Math.max(end - groupLength, 0);
  22. parts.unshift(value.slice(start, end));
  23. }
  24. }
  25. return parts.join('_');
  26. }
  27. function addSeparatorFromLeft(value, options) {
  28. return addSeparator(value, options, true);
  29. }
  30. function formatNumber(value, options) {
  31. const {integer, dot, fractional} = numeric.parseFloatNumber(value);
  32. return addSeparator(integer, options) + dot + addSeparatorFromLeft(fractional, options);
  33. }
  34. function format(value, {prefix, data}, options) {
  35. const formatOption = options[prefix.toLowerCase()];
  36. if (prefix) {
  37. return prefix + addSeparator(data, formatOption);
  38. }
  39. const {
  40. number,
  41. mark,
  42. sign,
  43. power,
  44. } = numeric.parseNumber(value);
  45. return formatNumber(number, formatOption) + mark + sign + addSeparator(power, options['']);
  46. }
  47. const defaultOptions = {
  48. binary: {minimumDigits: 0, groupLength: 4},
  49. octal: {minimumDigits: 0, groupLength: 4},
  50. hexadecimal: {minimumDigits: 0, groupLength: 2},
  51. number: {minimumDigits: 5, groupLength: 3},
  52. };
  53. const create = context => {
  54. const {
  55. onlyIfContainsSeparator,
  56. binary,
  57. octal,
  58. hexadecimal,
  59. number,
  60. } = {
  61. onlyIfContainsSeparator: false,
  62. ...context.options[0],
  63. };
  64. const options = {
  65. '0b': {
  66. onlyIfContainsSeparator,
  67. ...defaultOptions.binary,
  68. ...binary,
  69. },
  70. '0o': {
  71. onlyIfContainsSeparator,
  72. ...defaultOptions.octal,
  73. ...octal,
  74. },
  75. '0x': {
  76. onlyIfContainsSeparator,
  77. ...defaultOptions.hexadecimal,
  78. ...hexadecimal,
  79. },
  80. '': {
  81. onlyIfContainsSeparator,
  82. ...defaultOptions.number,
  83. ...number,
  84. },
  85. };
  86. return {
  87. Literal(node) {
  88. if (!numeric.isNumeric(node) || numeric.isLegacyOctal(node)) {
  89. return;
  90. }
  91. const {raw} = node;
  92. let number = raw;
  93. let suffix = '';
  94. if (isBigIntLiteral(node)) {
  95. number = raw.slice(0, -1);
  96. suffix = 'n';
  97. }
  98. const strippedNumber = number.replace(/_/g, '');
  99. const {prefix, data} = numeric.getPrefix(strippedNumber);
  100. const {onlyIfContainsSeparator} = options[prefix.toLowerCase()];
  101. if (onlyIfContainsSeparator && !raw.includes('_')) {
  102. return;
  103. }
  104. const formatted = format(strippedNumber, {prefix, data}, options) + suffix;
  105. if (raw !== formatted) {
  106. return {
  107. node,
  108. messageId: MESSAGE_ID,
  109. fix: fixer => fixer.replaceText(node, formatted),
  110. };
  111. }
  112. },
  113. };
  114. };
  115. const formatOptionsSchema = ({minimumDigits, groupLength}) => ({
  116. type: 'object',
  117. additionalProperties: false,
  118. properties: {
  119. onlyIfContainsSeparator: {
  120. type: 'boolean',
  121. },
  122. minimumDigits: {
  123. type: 'integer',
  124. minimum: 0,
  125. default: minimumDigits,
  126. },
  127. groupLength: {
  128. type: 'integer',
  129. minimum: 1,
  130. default: groupLength,
  131. },
  132. },
  133. });
  134. const schema = [{
  135. type: 'object',
  136. additionalProperties: false,
  137. properties: {
  138. ...Object.fromEntries(
  139. Object.entries(defaultOptions).map(([type, options]) => [type, formatOptionsSchema(options)]),
  140. ),
  141. onlyIfContainsSeparator: {
  142. type: 'boolean',
  143. default: false,
  144. },
  145. },
  146. }];
  147. /** @type {import('eslint').Rule.RuleModule} */
  148. module.exports = {
  149. create,
  150. meta: {
  151. type: 'suggestion',
  152. docs: {
  153. description: 'Enforce the style of numeric separators by correctly grouping digits.',
  154. },
  155. fixable: 'code',
  156. schema,
  157. messages,
  158. },
  159. };