numeric-separators-style.js 3.7 KB

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