Skip to content

Commit 7ba3599

Browse files
committed
chore: add vendored numeric-separators-style ESLint rule
Vendored from eslint-plugin-unicorn to enforce readable numeric literals with underscores (e.g., 1_000_000 instead of 1000000). Adapted to use standard ESLint visitor pattern instead of unicorn's internal context.on() API.
1 parent 3267bd6 commit 7ba3599

File tree

3 files changed

+248
-3
lines changed

3 files changed

+248
-3
lines changed

eslint.config.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { defineConfig } from 'eslint/config';
66
import globals from 'globals';
77
import tseslint from 'typescript-eslint';
88

9+
import numericSeparatorsPlugin from './vendor/eslint-numeric-separators-style/index.js';
10+
911
// TODO: setup eslint-plugin-n
1012
export default defineConfig(
1113
jsPlugin.configs.recommended,
@@ -25,6 +27,7 @@ export default defineConfig(
2527
plugins: {
2628
'@perfectionist': perfectionist,
2729
'@stylistic': stylistic,
30+
'numeric-separators': numericSeparatorsPlugin,
2831
},
2932
rules: {
3033
'@perfectionist/sort-classes': ['error', { partitionByNewLine: true }],
@@ -42,7 +45,6 @@ export default defineConfig(
4245
],
4346
'@stylistic/lines-between-class-members': ['error', 'always'],
4447
'@stylistic/semi': 'error',
45-
4648
'@typescript-eslint/consistent-type-exports': [
4749
'error',
4850
{ fixMixedExportsWithInlineTypeSpecifier: true },
@@ -92,9 +94,9 @@ export default defineConfig(
9294

9395
// these 6 bytes add up
9496
'@typescript-eslint/require-await': 'off',
97+
9598
// I like my template expressions, tyvm
9699
'@typescript-eslint/restrict-template-expressions': 'off',
97-
98100
'@typescript-eslint/unified-signatures': [
99101
'error',
100102
{
@@ -111,6 +113,7 @@ export default defineConfig(
111113
'no-constructor-return': 'error',
112114

113115
'no-empty': ['error', { allowEmptyCatch: true }],
116+
114117
'no-restricted-syntax': [
115118
'error',
116119
{
@@ -121,6 +124,7 @@ export default defineConfig(
121124
},
122125
],
123126
'no-self-compare': 'error',
127+
'numeric-separators/numeric-separators-style': 'error',
124128
'object-shorthand': ['error', 'always'],
125129
'prefer-arrow-callback': 'error',
126130
semi: 'error',

tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
"exclude": [
2525
"astro.config.js",
2626
"test/integration/fixture/**",
27-
"test/fixtures/adapters/**"
27+
"test/fixtures/adapters/**",
28+
"vendor/**"
2829
],
2930
"files": [
3031
"examples/modestbench.config.js",
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
// @ts-nocheck
2+
/**
3+
* Vendored from eslint-plugin-unicorn by Sindre Sorhus
4+
* @see https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/rules/numeric-separators-style.js
5+
* @license MIT
6+
*/
7+
8+
// --- Inlined from rules/ast/literal.js ---
9+
const isNumericLiteral = (node) =>
10+
node.type === 'Literal' && typeof node.value === 'number';
11+
12+
const isBigIntLiteral = (node) =>
13+
node.type === 'Literal' && Boolean(node.bigint);
14+
15+
// --- Inlined from rules/utils/numeric.js ---
16+
const isNumeric = (node) => isNumericLiteral(node) || isBigIntLiteral(node);
17+
18+
const isLegacyOctal = (node) =>
19+
isNumericLiteral(node) && /^0\d+$/.test(node.raw);
20+
21+
const getPrefix = (text) => {
22+
let prefix = '';
23+
let data = text;
24+
25+
if (/^0[box]/i.test(text)) {
26+
prefix = text.slice(0, 2);
27+
data = text.slice(2);
28+
}
29+
30+
return { prefix, data };
31+
};
32+
33+
const parseNumber = (text) => {
34+
const {
35+
number,
36+
mark = '',
37+
sign = '',
38+
power = '',
39+
} = text.match(
40+
/^(?<number>[\d._]*?)(?:(?<mark>[Ee])(?<sign>[+-])?(?<power>[\d_]+))?$/,
41+
).groups;
42+
43+
return { number, mark, sign, power };
44+
};
45+
46+
const parseFloatNumber = (text) => {
47+
const parts = text.split('.');
48+
const [integer, fractional = ''] = parts;
49+
const dot = parts.length === 2 ? '.' : '';
50+
51+
return { integer, dot, fractional };
52+
};
53+
54+
// --- Main rule from rules/numeric-separators-style.js ---
55+
const MESSAGE_ID = 'numeric-separators-style';
56+
const messages = {
57+
[MESSAGE_ID]: 'Invalid group length in numeric value.',
58+
};
59+
60+
const addSeparator = (value, { minimumDigits, groupLength }, fromLeft) => {
61+
const { length } = value;
62+
63+
if (length < minimumDigits) {
64+
return value;
65+
}
66+
67+
const parts = [];
68+
if (fromLeft) {
69+
for (let start = 0; start < length; start += groupLength) {
70+
const end = Math.min(start + groupLength, length);
71+
parts.push(value.slice(start, end));
72+
}
73+
} else {
74+
for (let end = length; end > 0; end -= groupLength) {
75+
const start = Math.max(end - groupLength, 0);
76+
parts.unshift(value.slice(start, end));
77+
}
78+
}
79+
80+
return parts.join('_');
81+
};
82+
83+
const addSeparatorFromLeft = (value, options) => addSeparator(value, options, true);
84+
85+
const formatNumber = (value, options) => {
86+
const { integer, dot, fractional } = parseFloatNumber(value);
87+
return (
88+
addSeparator(integer, options) + dot + addSeparatorFromLeft(fractional, options)
89+
);
90+
};
91+
92+
const format = (value, { prefix, data }, options) => {
93+
const formatOption = options[prefix.toLowerCase()];
94+
95+
if (prefix) {
96+
return prefix + addSeparator(data, formatOption);
97+
}
98+
99+
const { number, mark, sign, power } = parseNumber(value);
100+
101+
return (
102+
formatNumber(number, formatOption) + mark + sign + addSeparator(power, options[''])
103+
);
104+
};
105+
106+
const defaultOptions = {
107+
binary: { minimumDigits: 0, groupLength: 4 },
108+
octal: { minimumDigits: 0, groupLength: 4 },
109+
hexadecimal: { minimumDigits: 0, groupLength: 2 },
110+
number: { minimumDigits: 5, groupLength: 3 },
111+
};
112+
113+
const create = (context) => {
114+
const { onlyIfContainsSeparator, binary, octal, hexadecimal, number } = {
115+
onlyIfContainsSeparator: false,
116+
...context.options[0],
117+
};
118+
119+
const options = {
120+
'0b': {
121+
onlyIfContainsSeparator,
122+
...defaultOptions.binary,
123+
...binary,
124+
},
125+
'0o': {
126+
onlyIfContainsSeparator,
127+
...defaultOptions.octal,
128+
...octal,
129+
},
130+
'0x': {
131+
onlyIfContainsSeparator,
132+
...defaultOptions.hexadecimal,
133+
...hexadecimal,
134+
},
135+
'': {
136+
onlyIfContainsSeparator,
137+
...defaultOptions.number,
138+
...number,
139+
},
140+
};
141+
142+
return {
143+
Literal(node) {
144+
if (!isNumeric(node) || isLegacyOctal(node)) {
145+
return;
146+
}
147+
148+
const { raw } = node;
149+
let num = raw;
150+
let suffix = '';
151+
if (isBigIntLiteral(node)) {
152+
num = raw.slice(0, -1);
153+
suffix = 'n';
154+
}
155+
156+
const strippedNumber = num.replaceAll('_', '');
157+
const { prefix, data } = getPrefix(strippedNumber);
158+
159+
const { onlyIfContainsSeparator: onlyIfContains } = options[prefix.toLowerCase()];
160+
if (onlyIfContains && !raw.includes('_')) {
161+
return;
162+
}
163+
164+
const formatted = format(strippedNumber, { prefix, data }, options) + suffix;
165+
166+
if (raw !== formatted) {
167+
context.report({
168+
node,
169+
messageId: MESSAGE_ID,
170+
fix: (fixer) => fixer.replaceText(node, formatted),
171+
});
172+
}
173+
},
174+
};
175+
};
176+
177+
const formatOptionsSchema = () => ({
178+
type: 'object',
179+
additionalProperties: false,
180+
properties: {
181+
onlyIfContainsSeparator: {
182+
type: 'boolean',
183+
},
184+
minimumDigits: {
185+
type: 'integer',
186+
minimum: 0,
187+
},
188+
groupLength: {
189+
type: 'integer',
190+
minimum: 1,
191+
},
192+
},
193+
});
194+
195+
const schema = [
196+
{
197+
type: 'object',
198+
additionalProperties: false,
199+
properties: {
200+
...Object.fromEntries(
201+
Object.entries(defaultOptions).map(([type]) => [type, formatOptionsSchema()]),
202+
),
203+
onlyIfContainsSeparator: {
204+
type: 'boolean',
205+
},
206+
},
207+
},
208+
];
209+
210+
/** @type {import('eslint').Rule.RuleModule} */
211+
const rule = {
212+
create,
213+
meta: {
214+
type: 'suggestion',
215+
docs: {
216+
description:
217+
'Enforce the style of numeric separators by correctly grouping digits.',
218+
},
219+
fixable: 'code',
220+
schema,
221+
defaultOptions: [
222+
{
223+
onlyIfContainsSeparator: false,
224+
binary: defaultOptions.binary,
225+
octal: defaultOptions.octal,
226+
hexadecimal: defaultOptions.hexadecimal,
227+
number: defaultOptions.number,
228+
},
229+
],
230+
messages,
231+
},
232+
};
233+
234+
// Export as an ESLint plugin
235+
export default {
236+
rules: {
237+
'numeric-separators-style': rule,
238+
},
239+
};
240+

0 commit comments

Comments
 (0)