@kikiutils/eslint-config 9.3.2 → 10.1.0

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.
@@ -1,4 +1,7 @@
1
- import type { Rule } from 'eslint';
1
+ import type {
2
+ AST,
3
+ Rule,
4
+ } from 'eslint';
2
5
 
3
6
  import type { TokenStore } from '../utils';
4
7
  import { defineScriptAndTemplateVisitors } from '../utils';
@@ -13,120 +16,188 @@ type ListItem = CallNode['arguments'][number] | FunctionNode['params'][number];
13
16
 
14
17
  export const consistentParameterLayout: Rule.RuleModule = {
15
18
  create(context) {
16
- const isTestFile = /\.test\.[jt]sx?$/.test(context.filename);
17
-
18
- function checkParameterLayout(
19
- node: Rule.Node,
20
- items: readonly ListItem[],
21
- tokenStore: TokenStore,
22
- isTemplate: boolean,
23
- ): void {
24
- const first = items[0];
25
- if (!first) return;
26
- const isSingleParameter = items.length === 1;
27
-
28
- // JSX argument layout belongs to the JSX formatting rules.
29
- const firstType: string = first.type;
30
- if (isSingleParameter && (firstType === 'JSXElement' || firstType === 'JSXFragment')) return;
31
- let openingParen;
32
- if (node.type === 'CallExpression' || node.type === 'NewExpression') {
33
- const call = node as CallNode;
34
- openingParen = tokenStore.getTokenAfter(
35
- call.typeArguments ?? call.callee,
36
- (token) => token.type === 'Punctuator' && token.value === '(',
37
- );
38
- } else {
39
- openingParen = tokenStore.getTokenBefore(first);
40
- }
19
+ const compactTestCalls = context.options[0]?.compactTestCalls === true;
41
20
 
42
- if (!openingParen || openingParen.value !== '(') return;
43
- // An unparenthesized arrow must not borrow its parent's delimiters.
44
- if (openingParen.range[0] < node.range![0]) return;
45
- const closingParen = node.type === 'CallExpression' || node.type === 'NewExpression'
46
- ? tokenStore.getLastToken(node)
47
- : tokenStore.getTokenAfter(items.at(-1)!, (token) => token.value === ')');
48
-
49
- const next = tokenStore.getTokenAfter(openingParen)!;
21
+ function createVisitor(tokenStore: TokenStore, isTemplate: boolean): Rule.RuleListener {
22
+ function checkTestCallLayout(node: CallNode): void {
23
+ const items = node.arguments;
24
+ if (!items.length) return;
25
+ const openingParen = tokenStore.getTokenAfter(
26
+ node.typeArguments ?? node.callee,
27
+ (token) => token.type === 'Punctuator' && token.value === '(',
28
+ )!;
29
+
30
+ const closingParen = tokenStore.getLastToken(node)!;
31
+ const previous = tokenStore.getTokenBefore(closingParen)!;
32
+ const gaps: Array<[AST.Token, AST.Token, string]> = [
33
+ [
34
+ openingParen,
35
+ tokenStore.getTokenAfter(openingParen)!,
36
+ '',
37
+ ],
38
+ [
39
+ previous,
40
+ closingParen,
41
+ '',
42
+ ],
43
+ ];
50
44
 
51
- if (isSingleParameter) {
52
- if (!closingParen || closingParen.value !== ')') return;
53
- let previous = tokenStore.getTokenBefore(closingParen)!;
54
- if (previous.value === ',') previous = tokenStore.getTokenBefore(previous)!;
55
- if (
56
- tokenStore.commentsExistBetween(openingParen, next)
57
- || tokenStore.commentsExistBetween(previous, closingParen)
58
- ) return;
45
+ for (let index = 1; index < items.length; index++) {
46
+ const comma = tokenStore.getTokensBetween(items[index - 1]!, items[index]!)
47
+ .find((token) => token.type === 'Punctuator' && token.value === ',')!;
48
+
49
+ gaps.push(
50
+ [
51
+ tokenStore.getTokenBefore(comma)!,
52
+ comma,
53
+ '',
54
+ ],
55
+ [
56
+ comma,
57
+ tokenStore.getTokenAfter(comma)!,
58
+ ' ',
59
+ ],
60
+ );
61
+ }
59
62
 
60
- const hasOpeningNewline = openingParen.loc.end.line !== next.loc.start.line;
61
- const hasClosingNewline = tokenStore.getTokenBefore(closingParen)!.loc.end.line
62
- !== closingParen.loc.start.line;
63
+ if (previous.value === ',') {
64
+ gaps.push([
65
+ tokenStore.getTokenBefore(previous)!,
66
+ previous,
67
+ '',
68
+ ]);
69
+ }
63
70
 
64
- if (hasOpeningNewline === hasClosingNewline) return;
71
+ // Keep the whole list's layout when a boundary comment prevents safe compaction.
72
+ if (gaps.some(([left, right]) => tokenStore.commentsExistBetween(left, right))) return;
73
+ const multilineGaps = gaps.filter(([left, right]) => left.loc.end.line !== right.loc.start.line);
74
+ if (!multilineGaps.length) return;
65
75
 
66
76
  context.report({
67
- fix: (fixer) => hasOpeningNewline
68
- ? fixer.insertTextBefore(closingParen, '\n')
69
- : fixer.insertTextAfter(openingParen, '\n'),
70
- messageId: 'singleBoundary',
77
+ fix: (fixer) => multilineGaps.map(([
78
+ left,
79
+ right,
80
+ text,
81
+ ]) => fixer.replaceTextRange(
82
+ [
83
+ left.range[1],
84
+ right.range[0],
85
+ ],
86
+ text,
87
+ )),
88
+ messageId: 'compactTestCall',
71
89
  node,
72
90
  });
73
-
74
- return;
75
91
  }
76
92
 
77
- if (!closingParen || openingParen.loc.start.line === closingParen.loc.end.line) return;
93
+ function checkParameterLayout(
94
+ node: CallNode | FunctionNode,
95
+ items: readonly ListItem[],
96
+ ): void {
97
+ const first = items[0];
98
+ if (!first) return;
99
+ const isSingleParameter = items.length === 1;
100
+
101
+ // JSX argument layout belongs to the JSX formatting rules.
102
+ const firstType: string = first.type;
103
+ if (isSingleParameter && (firstType === 'JSXElement' || firstType === 'JSXFragment')) return;
104
+ let openingParen;
105
+ if (node.type === 'CallExpression' || node.type === 'NewExpression') {
106
+ openingParen = tokenStore.getTokenAfter(
107
+ node.typeArguments ?? node.callee,
108
+ (token) => token.type === 'Punctuator' && token.value === '(',
109
+ );
110
+ } else {
111
+ openingParen = tokenStore.getTokenBefore(first);
112
+ }
78
113
 
79
- if (isTemplate) {
80
- // Script list formatters do not traverse Vue template expressions.
81
- const newlineOffsets: number[] = [];
82
- if (openingParen.loc.end.line === next.loc.start.line) newlineOffsets.push(openingParen.range[1]);
83
- for (let index = 1; index < items.length; index++) {
84
- const item = items[index]!;
85
- const comma = tokenStore.getTokensBetween(items[index - 1]!, item)
86
- .find((token) => token.value === ',');
114
+ if (!openingParen || openingParen.value !== '(') return;
115
+ // An unparenthesized arrow must not borrow its parent's delimiters.
116
+ if (openingParen.range[0] < node.range![0]) return;
117
+ const closingParen = node.type === 'CallExpression' || node.type === 'NewExpression'
118
+ ? tokenStore.getLastToken(node)
119
+ : tokenStore.getTokenAfter(items.at(-1)!, (token) => token.value === ')');
87
120
 
88
- if (comma && comma.loc.end.line === tokenStore.getTokenAfter(comma)!.loc.start.line) {
89
- newlineOffsets.push(comma.range[1]);
90
- }
91
- }
121
+ const next = tokenStore.getTokenAfter(openingParen)!;
122
+
123
+ if (isSingleParameter) {
124
+ if (!closingParen || closingParen.value !== ')') return;
125
+ let previous = tokenStore.getTokenBefore(closingParen)!;
126
+ if (previous.value === ',') previous = tokenStore.getTokenBefore(previous)!;
127
+ if (
128
+ tokenStore.commentsExistBetween(openingParen, next)
129
+ || tokenStore.commentsExistBetween(previous, closingParen)
130
+ ) return;
131
+
132
+ const hasOpeningNewline = openingParen.loc.end.line !== next.loc.start.line;
133
+ const hasClosingNewline = tokenStore.getTokenBefore(closingParen)!.loc.end.line
134
+ !== closingParen.loc.start.line;
92
135
 
93
- if (
94
- closingParen.value === ')'
95
- && tokenStore.getTokenBefore(closingParen)!.loc.end.line === closingParen.loc.start.line
96
- ) newlineOffsets.push(closingParen.range[0]);
136
+ if (hasOpeningNewline === hasClosingNewline) return;
97
137
 
98
- if (newlineOffsets.length) {
99
138
  context.report({
100
- fix: (fixer) => newlineOffsets.map((offset) => fixer.insertTextAfterRange(
101
- [
102
- offset,
103
- offset,
104
- ],
105
- '\n',
106
- )),
107
- messageId: 'newline',
108
- node: first,
139
+ fix: (fixer) => hasOpeningNewline
140
+ ? fixer.insertTextBefore(closingParen, '\n')
141
+ : fixer.insertTextAfter(openingParen, '\n'),
142
+ messageId: 'singleBoundary',
143
+ node,
109
144
  });
145
+
146
+ return;
110
147
  }
111
148
 
112
- return;
113
- }
149
+ if (!closingParen || openingParen.loc.start.line === closingParen.loc.end.line) return;
114
150
 
115
- if (openingParen.loc.end.line !== next.loc.start.line) return;
151
+ if (isTemplate) {
152
+ // Script list formatters do not traverse Vue template expressions.
153
+ const newlineOffsets: number[] = [];
154
+ if (openingParen.loc.end.line === next.loc.start.line) newlineOffsets.push(openingParen.range[1]);
155
+ for (let index = 1; index < items.length; index++) {
156
+ const item = items[index]!;
157
+ const comma = tokenStore.getTokensBetween(items[index - 1]!, item)
158
+ .find((token) => token.value === ',');
116
159
 
117
- context.report({
118
- fix: (fixer) => fixer.insertTextAfter(openingParen, '\n'),
119
- messageId: 'newline',
120
- node: first,
121
- });
122
- }
160
+ if (comma && comma.loc.end.line === tokenStore.getTokenAfter(comma)!.loc.start.line) {
161
+ newlineOffsets.push(comma.range[1]);
162
+ }
163
+ }
164
+
165
+ if (
166
+ closingParen.value === ')'
167
+ && tokenStore.getTokenBefore(closingParen)!.loc.end.line === closingParen.loc.start.line
168
+ ) newlineOffsets.push(closingParen.range[0]);
169
+
170
+ if (newlineOffsets.length) {
171
+ context.report({
172
+ fix: (fixer) => newlineOffsets.map((offset) => fixer.insertTextBeforeRange(
173
+ [
174
+ offset,
175
+ offset,
176
+ ],
177
+ '\n',
178
+ )),
179
+ messageId: 'newline',
180
+ node: first,
181
+ });
182
+ }
183
+
184
+ return;
185
+ }
186
+
187
+ if (openingParen.loc.end.line !== next.loc.start.line) return;
188
+
189
+ context.report({
190
+ fix: (fixer) => fixer.insertTextAfter(openingParen, '\n'),
191
+ messageId: 'newline',
192
+ node: first,
193
+ });
194
+ }
123
195
 
124
- function createVisitor(tokenStore: TokenStore, isTemplate: boolean): Rule.RuleListener {
125
196
  return {
126
- ArrowFunctionExpression: (node) => checkParameterLayout(node, node.params, tokenStore, isTemplate),
197
+ ArrowFunctionExpression: (node) => checkParameterLayout(node, node.params),
127
198
  CallExpression: (node) => {
128
199
  const { callee } = node;
129
- if (isTestFile) {
200
+ if (compactTestCalls) {
130
201
  const target = callee.type === 'MemberExpression'
131
202
  && !callee.computed
132
203
  && callee.property.type === 'Identifier'
@@ -137,27 +208,37 @@ export const consistentParameterLayout: Rule.RuleModule = {
137
208
  if (
138
209
  target.type === 'Identifier'
139
210
  && (target.name === 'describe' || target.name === 'it')
140
- ) return;
211
+ ) {
212
+ checkTestCallLayout(node);
213
+ return;
214
+ }
141
215
  }
142
216
 
143
- checkParameterLayout(node, node.arguments, tokenStore, isTemplate);
217
+ checkParameterLayout(node, node.arguments);
144
218
  },
145
- FunctionDeclaration: (node) => checkParameterLayout(node, node.params, tokenStore, isTemplate),
146
- FunctionExpression: (node) => checkParameterLayout(node, node.params, tokenStore, isTemplate),
147
- NewExpression: (node) => checkParameterLayout(node, node.arguments, tokenStore, isTemplate),
219
+ FunctionDeclaration: (node) => checkParameterLayout(node, node.params),
220
+ FunctionExpression: (node) => checkParameterLayout(node, node.params),
221
+ NewExpression: (node) => checkParameterLayout(node, node.arguments),
148
222
  };
149
223
  }
150
224
 
151
225
  return defineScriptAndTemplateVisitors(context.sourceCode, createVisitor);
152
226
  },
153
227
  meta: {
154
- docs: { description: 'Preserve consistent single-parameter boundaries and expand multiline parameter lists.' },
228
+ docs: { description: 'Keep parameter boundaries consistent, expand multiline lists, and compact test calls.' },
155
229
  fixable: 'whitespace',
156
230
  messages: {
231
+ compactTestCall: 'Keep test-call arguments beside their parentheses and each other.',
157
232
  newline: 'Start a multiline parameter list on a new line.',
158
233
  singleBoundary: 'Use matching newline boundaries around a single parameter.',
159
234
  },
160
- schema: [],
235
+ schema: [
236
+ {
237
+ additionalProperties: false,
238
+ properties: { compactTestCalls: { type: 'boolean' } },
239
+ type: 'object',
240
+ },
241
+ ],
161
242
  type: 'layout',
162
243
  },
163
244
  };
@@ -10,12 +10,20 @@ export const statementPadding: Rule.RuleModule = {
10
10
  for (let index = 1; index < statements.length; index++) {
11
11
  const previous = statements[index - 1]!;
12
12
  const next = statements[index]!;
13
+ if (
14
+ [
15
+ previous,
16
+ next,
17
+ ].some((node) =>
18
+ node.type === 'ImportDeclaration'
19
+ || node.type === 'ExportAllDeclaration'
20
+ || (node.type === 'ExportNamedDeclaration' && node.source))
21
+ ) continue;
22
+
23
+ const previousFirstToken = sourceCode.getFirstToken(previous)!;
13
24
  const lastToken = sourceCode.getLastToken(previous)!;
14
25
  const firstToken = sourceCode.getFirstToken(next)!;
15
- const closingLine = sourceCode.lines[lastToken.loc.end.line - 1]!;
16
- const openingLine = sourceCode.lines[firstToken.loc.start.line - 1]!;
17
- const hasDifferentIndent = /^\s*/.exec(closingLine)![0] !== /^\s*/.exec(openingLine)![0];
18
- const hasStandaloneClosing = /^[)\]}]+;?$/.test(closingLine.slice(0, lastToken.loc.end.column).trim());
26
+ const isMultiline = previousFirstToken.loc.start.line !== lastToken.loc.end.line;
19
27
  const blockNode = previous.type === 'ExportDefaultDeclaration'
20
28
  || previous.type === 'ExportNamedDeclaration'
21
29
  ? previous.declaration
@@ -36,24 +44,27 @@ export const statementPadding: Rule.RuleModule = {
36
44
  'WithStatement',
37
45
  ].includes(blockNode.type);
38
46
 
39
- if (!hasDifferentIndent && !hasStandaloneClosing && !hasBlockClosing) continue;
40
- const comments = sourceCode.getCommentsBefore(next);
41
- if (
42
- sourceCode
43
- .lines
44
- .slice(lastToken.loc.end.line, firstToken.loc.start.line - 1)
45
- .some((line, index) => !line.trim() && !comments.some((comment) =>
46
- comment.loc!.start.line <= lastToken.loc.end.line + index + 1
47
- && comment.loc!.end.line >= lastToken.loc.end.line + index + 1))
48
- ) continue;
47
+ if (!isMultiline && !hasBlockClosing) continue;
48
+ let previousEnd = lastToken.range[1];
49
+ let previousEndLine = lastToken.loc.end.line;
50
+ let nextStart = firstToken.range[0];
51
+ let nextStartLine = firstToken.loc.start.line;
49
52
 
50
- // Keep leading comments attached to the next statement and trailing comments in place.
51
- const boundary = comments
52
- .find((comment) => comment.loc!.start.line > lastToken.loc.end.line) ?? firstToken;
53
+ // Include trailing comments in the previous boundary, not leading comments of the next statement.
54
+ for (const comment of sourceCode.getCommentsBefore(next)) {
55
+ if (comment.loc!.start.line === previousEndLine) {
56
+ previousEnd = comment.range![1];
57
+ previousEndLine = comment.loc!.end.line;
58
+ } else {
59
+ nextStart = comment.range![0];
60
+ nextStartLine = comment.loc!.start.line;
61
+ break;
62
+ }
63
+ }
53
64
 
54
- const offset = boundary.loc!.start.line > lastToken.loc.end.line
55
- ? boundary.range![0] - boundary.loc!.start.column
56
- : boundary.range![0];
65
+ if (nextStartLine > previousEndLine + 1) continue;
66
+ const linebreakIndex = sourceCode.text.slice(previousEnd, nextStart).indexOf('\n');
67
+ const offset = linebreakIndex === -1 ? nextStart : previousEnd + linebreakIndex + 1;
57
68
 
58
69
  context.report({
59
70
  fix: (fixer) => fixer.insertTextBeforeRange(
@@ -61,7 +72,7 @@ export const statementPadding: Rule.RuleModule = {
61
72
  offset,
62
73
  offset,
63
74
  ],
64
- boundary.loc!.start.line === lastToken.loc.end.line ? '\n\n' : '\n',
75
+ linebreakIndex === -1 ? '\n\n' : '\n',
65
76
  ),
66
77
  messageId: 'blankLine',
67
78
  node: next,
@@ -77,7 +88,7 @@ export const statementPadding: Rule.RuleModule = {
77
88
  };
78
89
  },
79
90
  meta: {
80
- docs: { description: 'Separate statements after closing lines, blocks, or indentation changes.' },
91
+ docs: { description: 'Separate complete multiline statements and block-ending statements with blank lines.' },
81
92
  fixable: 'whitespace',
82
93
  messages: { blankLine: 'Add a blank line between these complete statements.' },
83
94
  schema: [],
@@ -0,0 +1,28 @@
1
+ import type { TypedFlatConfigItem } from '@antfu/eslint-config';
2
+
3
+ export function createTypeAwareConfig(): TypedFlatConfigItem {
4
+ return {
5
+ languageOptions: {
6
+ parserOptions: {
7
+ projectService: true,
8
+ tsconfigRootDir: process.cwd(),
9
+ },
10
+ },
11
+ rules: {
12
+ 'require-await': 'off',
13
+ 'ts/await-thenable': 'error',
14
+ 'ts/no-base-to-string': 'error',
15
+ 'ts/no-floating-promises': 'error',
16
+ 'ts/no-for-in-array': 'error',
17
+ 'ts/no-misused-promises': 'error',
18
+ 'ts/no-misused-spread': 'error',
19
+ 'ts/no-unnecessary-type-assertion': 'error',
20
+ 'ts/require-await': 'error',
21
+ 'ts/return-await': 'error',
22
+ 'ts/switch-exhaustiveness-check': [
23
+ 'error',
24
+ { considerDefaultExhaustiveForUnions: true },
25
+ ],
26
+ },
27
+ };
28
+ }