lint-rules-alvin 1.2.1 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/.vscode/settings.json +8 -1
  2. package/eslint/configs/{astro.js → astro.ts} +2 -3
  3. package/eslint/configs/{base.js → base.ts} +2 -3
  4. package/eslint/configs/{custom.js → custom.ts} +10 -12
  5. package/eslint/configs/{importX.js → importX.ts} +13 -3
  6. package/eslint/configs/index.d.ts +11 -13
  7. package/eslint/configs/index.ts +10 -0
  8. package/eslint/configs/json.ts +7 -0
  9. package/eslint/configs/{markdown.js → markdown.ts} +2 -4
  10. package/eslint/configs/{reactHooks.js → reactHooks.ts} +2 -3
  11. package/eslint/configs/{stylistic.js → stylistic.ts} +2 -4
  12. package/eslint/configs/{sveltekit.js → sveltekit.ts} +4 -7
  13. package/eslint/configs/{typescript.js → typescript.ts} +4 -4
  14. package/eslint/custom_rules/{destructure-newline.js → destructure-newline.ts} +19 -14
  15. package/eslint/custom_rules/jsx-multiline-prop-newline.ts +245 -0
  16. package/eslint/custom_rules/max-chain-per-line.ts +320 -0
  17. package/eslint/custom_rules/multiline-array-accessor-newline.ts +249 -0
  18. package/eslint/custom_rules/{multiline-paren-newline.js → multiline-paren-newline.ts} +130 -65
  19. package/eslint/custom_rules/{newline-between-imports.js → newline-between-imports.ts} +16 -14
  20. package/eslint/custom_rules/unnamed-imports-last.ts +105 -0
  21. package/eslint/presets/{astroReactTs.js → astroReact.ts} +13 -11
  22. package/eslint/presets/index.d.ts +3 -11
  23. package/eslint/presets/index.ts +2 -0
  24. package/eslint/presets/{sveltekitTs.js → sveltekit.ts} +9 -7
  25. package/eslint.config.ts +19 -0
  26. package/package.json +51 -23
  27. package/test/eslint/destructure-newline.test.ts +21 -0
  28. package/test/eslint/jsx-multiline-prop-newline.test.ts +203 -0
  29. package/test/eslint/max-chain-per-line.test.ts +238 -0
  30. package/test/eslint/multiline-array-accessor-newline.test.ts +37 -0
  31. package/test/eslint/multiline-paren-newline.test.ts +87 -0
  32. package/test/eslint/newline-between-imports.test.ts +20 -0
  33. package/test/eslint/test-bootstrap.mjs +28 -0
  34. package/test/eslint/unnamed-imports-last.test.ts +25 -0
  35. package/tsconfig.json +25 -0
  36. package/tsconfig.test.json +18 -0
  37. package/eslint/configs/importXTs.js +0 -24
  38. package/eslint/configs/index.js +0 -11
  39. package/eslint/configs/json.js +0 -25
  40. package/eslint/custom_rules/jsx-multiline-prop-newline.js +0 -208
  41. package/eslint/custom_rules/max-chain-per-line.js +0 -253
  42. package/eslint/custom_rules/multiline-array-accessor-newline.js +0 -193
  43. package/eslint/custom_rules/unnamed-imports-last.js +0 -82
  44. package/eslint/presets/index.js +0 -2
  45. package/eslint.config.js +0 -17
@@ -0,0 +1,320 @@
1
+ import {
2
+ AST_NODE_TYPES,
3
+ ESLintUtils,
4
+ TSESTree
5
+ } from '@typescript-eslint/utils';
6
+
7
+ /*
8
+ * We use `any` for node parameters to keep the rule implementation
9
+ * lightweight for the TypeScript language server. Pragmatic runtime
10
+ * }
11
+ */
12
+
13
+ export const maxChainPerLineRule = ESLintUtils.RuleCreator.withoutDocs({
14
+ meta: {
15
+ type: 'layout',
16
+ docs: { description: 'Require a newline after each item in a chain if it\'s too long.' },
17
+ fixable: 'whitespace',
18
+ schema: [
19
+ {
20
+ type: 'object',
21
+ properties: {
22
+ maxChain: {
23
+ type: 'integer',
24
+ minimum: 1
25
+ },
26
+ enforceSingleLine: { type: 'boolean' }
27
+
28
+ },
29
+ additionalProperties: false
30
+ }
31
+ ],
32
+ messages: {
33
+ expand: 'This call chain is too long and should be broken into multiple lines.',
34
+ collapse: 'Unexpected newline in chain.'
35
+ }
36
+ },
37
+ defaultOptions: [
38
+ {
39
+ maxChain: 2,
40
+ enforceSingleLine: false
41
+ }
42
+ ],
43
+ create(context, options) {
44
+
45
+ const {
46
+ maxChain,
47
+ enforceSingleLine
48
+ } = options[0];
49
+ const sourceCode = context.sourceCode;
50
+ const processedChains: Set<TSESTree.Node> = new Set();
51
+
52
+ /**
53
+ * Traverses the AST from a given node upwards to find the outermost
54
+ * node of a continuous chain.
55
+ */
56
+ function getOutermostChainNode(node: TSESTree.Node) {
57
+
58
+ let outermostNode = node;
59
+ while (outermostNode.parent) {
60
+
61
+ const parentNode = outermostNode.parent;
62
+ if (
63
+ (parentNode.type === AST_NODE_TYPES.MemberExpression && parentNode.object === outermostNode)
64
+ || (parentNode.type === AST_NODE_TYPES.CallExpression && parentNode.callee === outermostNode)
65
+ || (parentNode.type === AST_NODE_TYPES.TSNonNullExpression && parentNode.expression === outermostNode)
66
+ ) {
67
+
68
+ outermostNode = parentNode;
69
+
70
+ } else {
71
+
72
+ break;
73
+
74
+ }
75
+
76
+ }
77
+ return outermostNode;
78
+
79
+ }
80
+
81
+ /**
82
+ * Deconstructs a chain into an array of its 'links'.
83
+ * A link is defined as a non-computed property access (`.prop`).
84
+ * Any subsequent CallExpressions or computed accesses (`[key]`) are
85
+ * considered part of that same link.
86
+ */
87
+ function getChainLinks(node: TSESTree.Node): TSESTree.MemberExpressionNonComputedName[] {
88
+
89
+ const links: TSESTree.MemberExpressionNonComputedName[] = [];
90
+ let currentNode: TSESTree.Node | undefined = node;
91
+
92
+ while (
93
+ currentNode.type === AST_NODE_TYPES.MemberExpression
94
+ || currentNode.type === AST_NODE_TYPES.CallExpression
95
+ || currentNode.type === AST_NODE_TYPES.TSNonNullExpression
96
+ ) {
97
+
98
+ if (currentNode.type === AST_NODE_TYPES.CallExpression) {
99
+
100
+ currentNode = currentNode.callee;
101
+ continue;
102
+
103
+ }
104
+
105
+ if (currentNode.type === AST_NODE_TYPES.TSNonNullExpression) {
106
+
107
+ currentNode = currentNode.expression;
108
+ continue;
109
+
110
+ }
111
+
112
+ // MemberExpression
113
+ if (currentNode.computed) {
114
+
115
+ currentNode = currentNode.object;
116
+
117
+ } else {
118
+
119
+ links.unshift(currentNode);
120
+ currentNode = currentNode.object;
121
+
122
+ }
123
+
124
+ }
125
+
126
+ return links;
127
+
128
+ }
129
+
130
+ function checkChain(node: TSESTree.Node): void {
131
+
132
+ const outermostNode = getOutermostChainNode(node);
133
+
134
+ if (processedChains.has(outermostNode)) {
135
+
136
+ return;
137
+
138
+ }
139
+ processedChains.add(outermostNode);
140
+
141
+ const links = getChainLinks(outermostNode);
142
+ const chainCount = links.length;
143
+
144
+ /*
145
+ * If enforceSingleLine is enabled, allow single-link chains to be
146
+ * considered for collapsing, so they can be collapsed if needed.
147
+ * Otherwise require at least 2 links.
148
+ */
149
+ const minChain = enforceSingleLine
150
+ ? 1
151
+ : 2;
152
+ if (chainCount < minChain) {
153
+
154
+ return;
155
+
156
+ }
157
+
158
+ const startLineIndex = outermostNode
159
+ .loc
160
+ .start
161
+ .line - 1;
162
+ const startLine = sourceCode.lines[startLineIndex] ?? '';
163
+ const match = (/^\s*/).exec(startLine);
164
+ const baseIndent = match
165
+ ? match[0]
166
+ : '';
167
+
168
+ // --- Mode 1: Enforce newlines if chain is too long ---
169
+ if (chainCount > maxChain) {
170
+
171
+ for (const linkNode of links) {
172
+
173
+ // Find the dot separator for this link.
174
+ const separatorToken = sourceCode.getTokenBefore(
175
+ linkNode.property,
176
+ { filter: (token) => token.value === '.' || token.value === '?.' }
177
+ );
178
+
179
+ // If we couldn't find the separator token, bail out for safety.
180
+ if (!separatorToken) {
181
+
182
+ continue;
183
+
184
+ }
185
+
186
+ // We only care about the dot's position relative to the start of its own link.
187
+ const previousNode = linkNode.object;
188
+ if (
189
+ separatorToken
190
+ .loc
191
+ .start
192
+ .line === previousNode
193
+ .loc
194
+ .end
195
+ .line
196
+ ) {
197
+
198
+ context.report({
199
+ node: linkNode.property,
200
+ loc: separatorToken.loc,
201
+ messageId: 'expand',
202
+ fix: (fixer) => fixer.insertTextBefore(
203
+ separatorToken,
204
+ `\n${baseIndent}`
205
+ )
206
+ });
207
+
208
+ }
209
+
210
+ }
211
+
212
+ } else if (enforceSingleLine) {
213
+
214
+ // --- Mode 2: Enforce single line if chain is short enough and option is enabled ---
215
+
216
+ for (const linkNode of links) {
217
+
218
+ const previousNode = linkNode.object;
219
+ const separatorToken2 = sourceCode.getTokenBefore(
220
+ linkNode.property,
221
+ { filter: (token) => token.value === '.' || token.value === '?.' }
222
+ );
223
+ if (!separatorToken2)
224
+ continue;
225
+ if (
226
+ separatorToken2
227
+ .loc
228
+ .start
229
+ .line > previousNode
230
+ .loc
231
+ .end
232
+ .line
233
+ ) {
234
+
235
+ const from = previousNode.range[1];
236
+ const to = separatorToken2.range[0];
237
+
238
+ if (typeof from === 'number' && typeof to === 'number') {
239
+
240
+ context.report({
241
+ node: linkNode.property,
242
+ loc: separatorToken2.loc,
243
+ messageId: 'collapse',
244
+ fix: (fixer) => {
245
+
246
+ const between = sourceCode.text.slice(
247
+ from,
248
+ to
249
+ );
250
+
251
+ // If there are non-whitespace chars (comments) inside, skip fixing.
252
+ if ((/\S/).test(between)) {
253
+
254
+ return null;
255
+
256
+ }
257
+
258
+ const segments = between.split(
259
+ /\r\n|\r|\n/
260
+ );
261
+
262
+ let collapsed = '';
263
+
264
+ if (segments.length === 1) {
265
+
266
+ collapsed = segments[0] ?? '';
267
+
268
+ } else {
269
+
270
+ const firstSegment = segments[0] ?? '';
271
+ const lastSegment = segments[segments.length - 1] ?? '';
272
+
273
+ collapsed = `${firstSegment}${lastSegment}`;
274
+
275
+ }
276
+
277
+ return fixer.replaceTextRange(
278
+ [
279
+ from,
280
+ to
281
+ ],
282
+ collapsed
283
+ );
284
+
285
+ }
286
+ });
287
+
288
+ } else {
289
+
290
+ context.report({
291
+ node: linkNode.property,
292
+ loc: separatorToken2.loc,
293
+ messageId: 'collapse'
294
+ });
295
+
296
+ }
297
+
298
+ }
299
+
300
+ }
301
+
302
+ }
303
+
304
+ }
305
+
306
+ return {
307
+ MemberExpression(node) {
308
+
309
+ // Only start the check from non-computed MemberExpressions to avoid redundant checks.
310
+ if (!node.computed) {
311
+
312
+ checkChain(node);
313
+
314
+ }
315
+
316
+ }
317
+ };
318
+
319
+ }
320
+ });
@@ -0,0 +1,249 @@
1
+ import {
2
+ ESLintUtils,
3
+ TSESTree
4
+ } from '@typescript-eslint/utils';
5
+
6
+ export const multilineArrayAccessorNewlineRule = ESLintUtils.RuleCreator.withoutDocs({
7
+ meta: {
8
+ type: 'layout',
9
+ docs: { description: 'Require newlines inside multiline array accessors' },
10
+ fixable: 'whitespace',
11
+ schema: [],
12
+ messages: {
13
+ expandAfter: 'Expected a newline after \'\'',
14
+ expandBefore: 'Expected a newline before \'\']\'',
15
+ collapse: 'Array accessor content should be on a single line.'
16
+ }
17
+ },
18
+ defaultOptions: [],
19
+ create(context) {
20
+
21
+ const sourceCode = context.sourceCode;
22
+
23
+ function checkMemberExpression(node: TSESTree.MemberExpression) {
24
+
25
+ const openBracket = sourceCode.getTokenAfter(
26
+ node.object,
27
+ { filter: (t) => t.value === '[' }
28
+ );
29
+
30
+ if (!openBracket) {
31
+
32
+ return;
33
+
34
+ }
35
+
36
+ const closeBracket = sourceCode.getLastToken(node);
37
+
38
+ if (closeBracket?.value !== ']') {
39
+
40
+ return;
41
+
42
+ }
43
+
44
+ // If the brackets are on the same line, this rule doesn't apply.
45
+ if (
46
+ openBracket
47
+ .loc
48
+ .start
49
+ .line === closeBracket
50
+ .loc
51
+ .end
52
+ .line
53
+ ) {
54
+
55
+ return;
56
+
57
+ }
58
+
59
+ const firstTokenOfProperty = sourceCode.getFirstToken(node.property);
60
+ const lastTokenOfProperty = sourceCode.getLastToken(node.property);
61
+
62
+ if (!firstTokenOfProperty || !lastTokenOfProperty) {
63
+
64
+ return;
65
+
66
+ }
67
+
68
+ // Check if property is on a single line
69
+ if (
70
+ firstTokenOfProperty
71
+ .loc
72
+ .start
73
+ .line === lastTokenOfProperty
74
+ .loc
75
+ .end
76
+ .line
77
+ ) {
78
+
79
+ const openBracketOnOwnLine = openBracket
80
+ .loc
81
+ .end
82
+ .line !== firstTokenOfProperty
83
+ .loc
84
+ .start
85
+ .line;
86
+ const closeBracketOnOwnLine = lastTokenOfProperty
87
+ .loc
88
+ .end
89
+ .line !== closeBracket
90
+ .loc
91
+ .start
92
+ .line;
93
+
94
+ if (openBracketOnOwnLine || closeBracketOnOwnLine) {
95
+
96
+ /*
97
+ * Report separate errors for opening and closing brackets so each
98
+ * bracket produces its own message and fix.
99
+ */
100
+ if (openBracketOnOwnLine) {
101
+
102
+ context.report({
103
+ node: openBracket,
104
+ messageId: 'collapse',
105
+ fix(fixer) {
106
+
107
+ const start = openBracket.range[1];
108
+ const end = firstTokenOfProperty.range[0];
109
+ const between = sourceCode.text.slice(
110
+ start,
111
+ end
112
+ );
113
+
114
+ // No fixer when comments inside
115
+ if ((/\S/).test(between) && !(/^\s*$/).test(between)) {
116
+
117
+ return null;
118
+
119
+ }
120
+
121
+ // Preserve any spaces that are on the same line as the first token.
122
+ const lastNewline = between.lastIndexOf('\n');
123
+ const preserve = lastNewline >= 0
124
+ ? between.slice(lastNewline + 1)
125
+ : between;
126
+
127
+ return fixer.replaceTextRange(
128
+ [
129
+ start,
130
+ end
131
+ ],
132
+ preserve
133
+ );
134
+
135
+ }
136
+ });
137
+
138
+ }
139
+
140
+ if (closeBracketOnOwnLine) {
141
+
142
+ context.report({
143
+ node: closeBracket,
144
+ messageId: 'collapse',
145
+ fix(fixer) {
146
+
147
+ const start = lastTokenOfProperty.range[1];
148
+ const end = closeBracket.range[0];
149
+ const between = sourceCode.text.slice(
150
+ start,
151
+ end
152
+ );
153
+
154
+ // If there are non-whitespace chars (comments) inside, skip fixing.
155
+ if ((/\S/).test(between) && !(/^\s*$/).test(between)) {
156
+
157
+ return null;
158
+
159
+ }
160
+
161
+ // Preserve any spaces that are on the same line as the last token.
162
+ const firstNewline = between.indexOf('\n');
163
+ const preserve = firstNewline >= 0
164
+ ? between.slice(
165
+ 0,
166
+ firstNewline
167
+ )
168
+ : between;
169
+
170
+ return fixer.replaceTextRange(
171
+ [
172
+ start,
173
+ end
174
+ ],
175
+ preserve
176
+ );
177
+
178
+ }
179
+ });
180
+
181
+ }
182
+
183
+ }
184
+ return;
185
+
186
+ }
187
+
188
+ // Check for a newline after the opening bracket.
189
+ if (
190
+ openBracket
191
+ .loc
192
+ .end
193
+ .line === firstTokenOfProperty
194
+ .loc
195
+ .start
196
+ .line
197
+ ) {
198
+
199
+ context.report({
200
+ node: openBracket,
201
+ messageId: 'expandAfter',
202
+ fix: (fixer) => fixer.insertTextAfter(
203
+ openBracket,
204
+ '\n'
205
+ )
206
+ });
207
+
208
+ }
209
+
210
+ // Check for a newline before the closing bracket.
211
+ if (
212
+ lastTokenOfProperty
213
+ .loc
214
+ .end
215
+ .line === closeBracket
216
+ .loc
217
+ .start
218
+ .line
219
+ ) {
220
+
221
+ context.report({
222
+ node: closeBracket,
223
+ messageId: 'expandBefore',
224
+ fix: (fixer) => fixer.insertTextBefore(
225
+ closeBracket,
226
+ '\n'
227
+ )
228
+ });
229
+
230
+ }
231
+
232
+ }
233
+
234
+ return {
235
+ MemberExpression(node) {
236
+
237
+ if (!node.computed) {
238
+
239
+ return;
240
+
241
+ }
242
+
243
+ checkMemberExpression(node);
244
+
245
+ }
246
+ };
247
+
248
+ }
249
+ });