@timmo001/oxlint-rules 0.3.0 → 0.3.2
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.
- package/README.md +2 -1
- package/THIRD_PARTY_NOTICES.md +9 -0
- package/dist/cli.js +978 -83
- package/dist/configs/recommended-effect.js +1207 -312
- package/dist/configs/recommended.js +772 -70
- package/dist/upstream/anti-slop.js +765 -63
- package/dist/upstream/effect.js +196 -3
- package/package.json +5 -5
- package/vendor/anti-slop/src/effect/index.ts +8 -0
- package/vendor/anti-slop/src/effect/rules/no-manual-effect-error-tag.ts +52 -0
- package/vendor/anti-slop/src/effect/rules/no-manual-tag-comparison.ts +45 -0
- package/vendor/anti-slop/src/effect/rules/no-manual-tagged-construction.ts +37 -0
- package/vendor/anti-slop/src/effect/rules/prefer-effect-match.ts +54 -0
- package/vendor/anti-slop/src/effect/shared/tagged-values.ts +97 -0
- package/vendor/anti-slop/src/index.ts +6 -0
- package/vendor/anti-slop/src/rules/no-array-filter-map.ts +28 -0
- package/vendor/anti-slop/src/rules/no-known-value-widening.ts +2 -14
- package/vendor/anti-slop/src/rules/no-module-mocking.ts +3 -14
- package/vendor/anti-slop/src/rules/no-reduce-accumulator-copy.ts +109 -0
- package/vendor/anti-slop/src/rules/require-readable-spacing.ts +47 -0
- package/vendor/anti-slop/src/shared/array-method.ts +94 -0
- package/vendor/anti-slop/src/shared/reflect-method.ts +2 -13
- package/vendor/anti-slop/src/shared/scope.ts +15 -0
- package/vendor/anti-slop/src/vendor/eslint-stylistic/LICENSE +22 -0
- package/vendor/anti-slop/src/vendor/eslint-stylistic/UPSTREAM.md +28 -0
- package/vendor/anti-slop/src/vendor/eslint-stylistic/padding-line-ast.ts +51 -0
- package/vendor/anti-slop/src/vendor/eslint-stylistic/padding-line-between-statements.ts +906 -0
- package/vendor/anti-slop/src/vendor/eslint-stylistic/padding-line-options.d.ts +87 -0
|
@@ -0,0 +1,906 @@
|
|
|
1
|
+
// Vendored from ESLint Stylistic; see UPSTREAM.md and LICENSE in this directory.
|
|
2
|
+
import type { ESTree, Context as RuleContext, SourceCode, Token as SyntaxToken, Comment, CreateRule, Location } from '@oxlint/plugins'
|
|
3
|
+
type ASTNode = ESTree.Node
|
|
4
|
+
type Token = SyntaxToken | Comment
|
|
5
|
+
import type {
|
|
6
|
+
RuleOptions,
|
|
7
|
+
SelectorOption,
|
|
8
|
+
StatementOption,
|
|
9
|
+
} from './padding-line-options.d.ts'
|
|
10
|
+
import {
|
|
11
|
+
isClosingBraceToken,
|
|
12
|
+
isFunction,
|
|
13
|
+
isNotSemicolonToken,
|
|
14
|
+
isParenthesized,
|
|
15
|
+
isSemicolonToken,
|
|
16
|
+
isSingleLine,
|
|
17
|
+
isTokenOnSameLine,
|
|
18
|
+
isTopLevelExpressionStatement,
|
|
19
|
+
LINEBREAKS,
|
|
20
|
+
skipChainExpression,
|
|
21
|
+
} from './padding-line-ast.ts'
|
|
22
|
+
|
|
23
|
+
const CJS_EXPORT = /^(?:module\s*\.\s*)?exports(?:\s*\.|\s*\[|$)/u
|
|
24
|
+
const CJS_IMPORT = /^require\(/u
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* This rule is a replica of padding-line-between-statements.
|
|
28
|
+
*
|
|
29
|
+
* Ideally we would want to extend the rule support typescript specific support.
|
|
30
|
+
* But since not all the state is exposed by the eslint and eslint has frozen stylistic rules,
|
|
31
|
+
* (see - https://eslint.org/blog/2020/05/changes-to-rules-policies for details.)
|
|
32
|
+
* we are forced to re-implement the rule here.
|
|
33
|
+
*
|
|
34
|
+
* We have tried to keep the implementation as close as possible to the eslint implementation, to make
|
|
35
|
+
* patching easier for future contributors.
|
|
36
|
+
*
|
|
37
|
+
* Reference rule - https://github.com/eslint/eslint/blob/main/lib/rules/padding-line-between-statements.js
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
type NodeTest = (
|
|
41
|
+
node: ASTNode,
|
|
42
|
+
sourceCode: SourceCode,
|
|
43
|
+
) => boolean
|
|
44
|
+
|
|
45
|
+
interface NodeTestObject {
|
|
46
|
+
test: NodeTest
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const LT = `[${Array.from(LINEBREAKS).join('')}]`
|
|
50
|
+
const PADDING_LINE_SEQUENCE = new RegExp(
|
|
51
|
+
String.raw`^(\s*?${LT})\s*${LT}(\s*;?)$`,
|
|
52
|
+
'u',
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
function isSelectorOption(option: StatementOption): option is SelectorOption {
|
|
56
|
+
return typeof option === 'object' && !Array.isArray(option)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Creates tester which check if a node starts with specific keyword with the
|
|
61
|
+
* appropriate AST_NODE_TYPES.
|
|
62
|
+
* @param keyword The keyword to test.
|
|
63
|
+
* @returns the created tester.
|
|
64
|
+
* @private
|
|
65
|
+
*/
|
|
66
|
+
function newKeywordTester(
|
|
67
|
+
type: string | string[],
|
|
68
|
+
keyword: string,
|
|
69
|
+
): NodeTestObject {
|
|
70
|
+
return {
|
|
71
|
+
test(node, sourceCode): boolean {
|
|
72
|
+
const isSameKeyword = sourceCode.getFirstToken(node)?.value === keyword
|
|
73
|
+
const isSameType = Array.isArray(type)
|
|
74
|
+
? type.includes(node.type)
|
|
75
|
+
: type === node.type
|
|
76
|
+
|
|
77
|
+
return isSameKeyword && isSameType
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Creates tester which check if a node is specific type.
|
|
84
|
+
* @param type The node type to test.
|
|
85
|
+
* @returns the created tester.
|
|
86
|
+
* @private
|
|
87
|
+
*/
|
|
88
|
+
function newNodeTypeTester(type: string): NodeTestObject {
|
|
89
|
+
return {
|
|
90
|
+
test: (node): boolean => node.type === type,
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Checks the given node is an expression statement of IIFE.
|
|
96
|
+
* @param node The node to check.
|
|
97
|
+
* @returns `true` if the node is an expression statement of IIFE.
|
|
98
|
+
* @private
|
|
99
|
+
*/
|
|
100
|
+
function isIIFEStatement(node: ASTNode): boolean {
|
|
101
|
+
if (node.type === 'ExpressionStatement') {
|
|
102
|
+
let expression = skipChainExpression(node.expression)
|
|
103
|
+
if (expression.type === 'UnaryExpression')
|
|
104
|
+
expression = skipChainExpression(expression.argument)
|
|
105
|
+
|
|
106
|
+
if (expression.type === 'CallExpression') {
|
|
107
|
+
let node: ASTNode = expression.callee
|
|
108
|
+
while (node.type === 'SequenceExpression') {
|
|
109
|
+
const lastExpression = node.expressions.at(-1)
|
|
110
|
+
if (lastExpression === undefined)
|
|
111
|
+
throw new Error('Padding rule invariant: sequence expression is empty')
|
|
112
|
+
node = lastExpression
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return isFunction(node)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return false
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Checks the given node is a CommonJS require statement
|
|
123
|
+
* @param node The node to check.
|
|
124
|
+
* @returns `true` if the node is a CommonJS require statement.
|
|
125
|
+
* @private
|
|
126
|
+
*/
|
|
127
|
+
function isCJSRequire(node: ASTNode): boolean {
|
|
128
|
+
if (node.type === 'VariableDeclaration') {
|
|
129
|
+
const declaration = node.declarations[0]
|
|
130
|
+
if (declaration?.init) {
|
|
131
|
+
let call = declaration?.init
|
|
132
|
+
while (call.type === 'MemberExpression')
|
|
133
|
+
call = call.object
|
|
134
|
+
|
|
135
|
+
if (
|
|
136
|
+
call.type === 'CallExpression'
|
|
137
|
+
&& call.callee.type === 'Identifier'
|
|
138
|
+
) {
|
|
139
|
+
return call.callee.name === 'require'
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return false
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Checks whether the given node is a block-like statement.
|
|
148
|
+
* This checks the last token of the node is the closing brace of a block.
|
|
149
|
+
* @param sourceCode The source code to get tokens.
|
|
150
|
+
* @param node The node to check.
|
|
151
|
+
* @returns `true` if the node is a block-like statement.
|
|
152
|
+
* @private
|
|
153
|
+
*/
|
|
154
|
+
function isBlockLikeStatement(
|
|
155
|
+
node: ASTNode,
|
|
156
|
+
sourceCode: SourceCode,
|
|
157
|
+
): boolean {
|
|
158
|
+
// do-while with a block is a block-like statement.
|
|
159
|
+
if (
|
|
160
|
+
node.type === 'DoWhileStatement'
|
|
161
|
+
&& node.body.type === 'BlockStatement'
|
|
162
|
+
) {
|
|
163
|
+
return true
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* IIFE is a block-like statement specially from
|
|
168
|
+
* JSCS#disallowPaddingNewLinesAfterBlocks.
|
|
169
|
+
*/
|
|
170
|
+
if (isIIFEStatement(node))
|
|
171
|
+
return true
|
|
172
|
+
|
|
173
|
+
// Checks the last token is a closing brace of blocks.
|
|
174
|
+
const lastToken = sourceCode.getLastToken(node, isNotSemicolonToken)
|
|
175
|
+
const belongingNode
|
|
176
|
+
= lastToken && isClosingBraceToken(lastToken)
|
|
177
|
+
? sourceCode.getNodeByRangeIndex(lastToken.range[0])
|
|
178
|
+
: null
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
!!belongingNode
|
|
182
|
+
&& (belongingNode.type === 'BlockStatement'
|
|
183
|
+
|| belongingNode.type === 'SwitchStatement')
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Check whether the given node is a directive or not.
|
|
189
|
+
* @param node The node to check.
|
|
190
|
+
* @param sourceCode The source code object to get tokens.
|
|
191
|
+
* @returns `true` if the node is a directive.
|
|
192
|
+
*/
|
|
193
|
+
function isDirective(
|
|
194
|
+
node: ASTNode,
|
|
195
|
+
sourceCode: SourceCode,
|
|
196
|
+
): boolean {
|
|
197
|
+
return (
|
|
198
|
+
isTopLevelExpressionStatement(node)
|
|
199
|
+
&& node.expression.type === 'Literal'
|
|
200
|
+
&& typeof node.expression.value === 'string'
|
|
201
|
+
&& !isParenthesized(node.expression, sourceCode)
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Check whether the given node is a part of directive prologue or not.
|
|
207
|
+
* @param node The node to check.
|
|
208
|
+
* @param sourceCode The source code object to get tokens.
|
|
209
|
+
* @returns `true` if the node is a part of directive prologue.
|
|
210
|
+
*/
|
|
211
|
+
function isDirectivePrologue(
|
|
212
|
+
node: ASTNode,
|
|
213
|
+
sourceCode: SourceCode,
|
|
214
|
+
): boolean {
|
|
215
|
+
if (
|
|
216
|
+
isDirective(node, sourceCode)
|
|
217
|
+
&& node.parent
|
|
218
|
+
&& 'body' in node.parent
|
|
219
|
+
&& Array.isArray(node.parent.body)
|
|
220
|
+
) {
|
|
221
|
+
for (const sibling of node.parent.body) {
|
|
222
|
+
if (sibling === node)
|
|
223
|
+
break
|
|
224
|
+
|
|
225
|
+
if (!isDirective(sibling, sourceCode))
|
|
226
|
+
return false
|
|
227
|
+
}
|
|
228
|
+
return true
|
|
229
|
+
}
|
|
230
|
+
return false
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Checks the given node is a CommonJS export statement
|
|
235
|
+
* @param node The node to check.
|
|
236
|
+
* @returns `true` if the node is a CommonJS export statement.
|
|
237
|
+
* @private
|
|
238
|
+
*/
|
|
239
|
+
function isCJSExport(node: ASTNode): boolean {
|
|
240
|
+
if (node.type === 'ExpressionStatement') {
|
|
241
|
+
const expression = node.expression
|
|
242
|
+
if (expression.type === 'AssignmentExpression') {
|
|
243
|
+
let left = expression.left
|
|
244
|
+
if (left.type === 'MemberExpression') {
|
|
245
|
+
while (left.object.type === 'MemberExpression')
|
|
246
|
+
left = left.object
|
|
247
|
+
|
|
248
|
+
return (
|
|
249
|
+
left.object.type === 'Identifier'
|
|
250
|
+
&& (left.object.name === 'exports'
|
|
251
|
+
|| (left.object.name === 'module'
|
|
252
|
+
&& left.property.type === 'Identifier'
|
|
253
|
+
&& left.property.name === 'exports'))
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return false
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Check whether the given node is an expression
|
|
263
|
+
* @param node The node to check.
|
|
264
|
+
* @param sourceCode The source code object to get tokens.
|
|
265
|
+
* @returns `true` if the node is an expression
|
|
266
|
+
*/
|
|
267
|
+
function isExpression(
|
|
268
|
+
node: ASTNode,
|
|
269
|
+
sourceCode: SourceCode,
|
|
270
|
+
): boolean {
|
|
271
|
+
return (
|
|
272
|
+
node.type === 'ExpressionStatement'
|
|
273
|
+
&& !isDirectivePrologue(node, sourceCode)
|
|
274
|
+
)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Gets the actual last token.
|
|
279
|
+
*
|
|
280
|
+
* If a semicolon is semicolon-less style's semicolon, this ignores it.
|
|
281
|
+
* For example:
|
|
282
|
+
*
|
|
283
|
+
* foo()
|
|
284
|
+
* ;[1, 2, 3].forEach(bar)
|
|
285
|
+
* @param sourceCode The source code to get tokens.
|
|
286
|
+
* @param node The node to get.
|
|
287
|
+
* @returns The actual last token.
|
|
288
|
+
* @private
|
|
289
|
+
*/
|
|
290
|
+
function getActualLastToken(
|
|
291
|
+
node: ASTNode,
|
|
292
|
+
sourceCode: SourceCode,
|
|
293
|
+
): Token | null {
|
|
294
|
+
const semiToken = sourceCode.getLastToken(node)!
|
|
295
|
+
const prevToken = sourceCode.getTokenBefore(semiToken)
|
|
296
|
+
const nextToken = sourceCode.getTokenAfter(semiToken)
|
|
297
|
+
const isSemicolonLessStyle
|
|
298
|
+
= prevToken
|
|
299
|
+
&& nextToken
|
|
300
|
+
&& prevToken.range[0] >= node.range[0]
|
|
301
|
+
&& isSemicolonToken(semiToken)
|
|
302
|
+
&& !isTokenOnSameLine(prevToken, semiToken)
|
|
303
|
+
&& isTokenOnSameLine(semiToken, nextToken)
|
|
304
|
+
|
|
305
|
+
return isSemicolonLessStyle ? prevToken : semiToken
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* This returns the concatenation of the first 2 captured strings.
|
|
310
|
+
* @param _ Unused. Whole matched string.
|
|
311
|
+
* @param trailingSpaces The trailing spaces of the first line.
|
|
312
|
+
* @param indentSpaces The indentation spaces of the last line.
|
|
313
|
+
* @returns The concatenation of trailingSpaces and indentSpaces.
|
|
314
|
+
* @private
|
|
315
|
+
*/
|
|
316
|
+
function replacerToRemovePaddingLines(
|
|
317
|
+
_: string,
|
|
318
|
+
trailingSpaces: string,
|
|
319
|
+
indentSpaces: string,
|
|
320
|
+
): string {
|
|
321
|
+
return trailingSpaces + indentSpaces
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function getReportLoc(node: ASTNode, sourceCode: SourceCode): Location {
|
|
325
|
+
if (isSingleLine(node))
|
|
326
|
+
return node.loc
|
|
327
|
+
|
|
328
|
+
const line = node.loc.start.line
|
|
329
|
+
const sourceLine = sourceCode.lines[line - 1]
|
|
330
|
+
if (sourceLine === undefined)
|
|
331
|
+
throw new Error('Padding rule invariant: statement source line is missing')
|
|
332
|
+
|
|
333
|
+
return {
|
|
334
|
+
start: node.loc.start,
|
|
335
|
+
end: {
|
|
336
|
+
line,
|
|
337
|
+
column: sourceLine.length,
|
|
338
|
+
},
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Check and report statements for `any` configuration.
|
|
344
|
+
* It does nothing.
|
|
345
|
+
*
|
|
346
|
+
* @private
|
|
347
|
+
*/
|
|
348
|
+
function verifyForAny(): void {
|
|
349
|
+
// Empty
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Check and report statements for `never` configuration.
|
|
354
|
+
* This autofix removes blank lines between the given 2 statements.
|
|
355
|
+
* However, if comments exist between 2 blank lines, it does not remove those
|
|
356
|
+
* blank lines automatically.
|
|
357
|
+
* @param context The rule context to report.
|
|
358
|
+
* @param _ Unused. The previous node to check.
|
|
359
|
+
* @param nextNode The next node to check.
|
|
360
|
+
* @param paddingLines The array of token pairs that blank
|
|
361
|
+
* lines exist between the pair.
|
|
362
|
+
*
|
|
363
|
+
* @private
|
|
364
|
+
*/
|
|
365
|
+
function verifyForNever(
|
|
366
|
+
context: RuleContext,
|
|
367
|
+
_: ASTNode,
|
|
368
|
+
nextNode: ASTNode,
|
|
369
|
+
paddingLines: [Token, Token][],
|
|
370
|
+
): void {
|
|
371
|
+
if (paddingLines.length === 0)
|
|
372
|
+
return
|
|
373
|
+
|
|
374
|
+
context.report({
|
|
375
|
+
node: nextNode,
|
|
376
|
+
messageId: 'unexpectedBlankLine',
|
|
377
|
+
loc: getReportLoc(nextNode, context.sourceCode),
|
|
378
|
+
fix(fixer) {
|
|
379
|
+
if (paddingLines.length >= 2)
|
|
380
|
+
return null
|
|
381
|
+
|
|
382
|
+
const paddingPair = paddingLines[0]
|
|
383
|
+
if (paddingPair === undefined)
|
|
384
|
+
throw new Error('Padding rule invariant: reported padding pair is missing')
|
|
385
|
+
const [prevToken, nextToken] = paddingPair
|
|
386
|
+
const start = prevToken.range[1]
|
|
387
|
+
const end = nextToken.range[0]
|
|
388
|
+
const text = context
|
|
389
|
+
.sourceCode
|
|
390
|
+
.text
|
|
391
|
+
.slice(start, end)
|
|
392
|
+
.replace(PADDING_LINE_SEQUENCE, replacerToRemovePaddingLines)
|
|
393
|
+
|
|
394
|
+
return fixer.replaceTextRange([start, end], text)
|
|
395
|
+
},
|
|
396
|
+
})
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Check and report statements for `always` configuration.
|
|
401
|
+
* This autofix inserts a blank line between the given 2 statements.
|
|
402
|
+
* If the `prevNode` has trailing comments, it inserts a blank line after the
|
|
403
|
+
* trailing comments.
|
|
404
|
+
* @param context The rule context to report.
|
|
405
|
+
* @param prevNode The previous node to check.
|
|
406
|
+
* @param nextNode The next node to check.
|
|
407
|
+
* @param paddingLines The array of token pairs that blank
|
|
408
|
+
* lines exist between the pair.
|
|
409
|
+
*
|
|
410
|
+
* @private
|
|
411
|
+
*/
|
|
412
|
+
function verifyForAlways(
|
|
413
|
+
context: RuleContext,
|
|
414
|
+
prevNode: ASTNode,
|
|
415
|
+
nextNode: ASTNode,
|
|
416
|
+
paddingLines: [Token, Token][],
|
|
417
|
+
): void {
|
|
418
|
+
if (paddingLines.length > 0)
|
|
419
|
+
return
|
|
420
|
+
|
|
421
|
+
context.report({
|
|
422
|
+
node: nextNode,
|
|
423
|
+
messageId: 'expectedBlankLine',
|
|
424
|
+
loc: getReportLoc(nextNode, context.sourceCode),
|
|
425
|
+
fix(fixer) {
|
|
426
|
+
const sourceCode = context.sourceCode
|
|
427
|
+
let prevToken = getActualLastToken(prevNode, sourceCode)!
|
|
428
|
+
const nextToken
|
|
429
|
+
= sourceCode.getFirstTokenBetween(prevToken, nextNode, {
|
|
430
|
+
includeComments: true,
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Skip the trailing comments of the previous node.
|
|
434
|
+
* This inserts a blank line after the last trailing comment.
|
|
435
|
+
*
|
|
436
|
+
* For example:
|
|
437
|
+
*
|
|
438
|
+
* foo(); // trailing comment.
|
|
439
|
+
* // comment.
|
|
440
|
+
* bar();
|
|
441
|
+
*
|
|
442
|
+
* Get fixed to:
|
|
443
|
+
*
|
|
444
|
+
* foo(); // trailing comment.
|
|
445
|
+
*
|
|
446
|
+
* // comment.
|
|
447
|
+
* bar();
|
|
448
|
+
* @param token The token to check.
|
|
449
|
+
* @returns `true` if the token is not a trailing comment.
|
|
450
|
+
* @private
|
|
451
|
+
*/
|
|
452
|
+
filter(token) {
|
|
453
|
+
if (isTokenOnSameLine(prevToken, token)) {
|
|
454
|
+
prevToken = token
|
|
455
|
+
return false
|
|
456
|
+
}
|
|
457
|
+
return true
|
|
458
|
+
},
|
|
459
|
+
})! || nextNode
|
|
460
|
+
const insertText = isTokenOnSameLine(prevToken, nextToken)
|
|
461
|
+
? '\n\n'
|
|
462
|
+
: '\n'
|
|
463
|
+
|
|
464
|
+
return fixer.insertTextAfter(prevToken, insertText)
|
|
465
|
+
},
|
|
466
|
+
})
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Types of blank lines.
|
|
471
|
+
* `any`, `never`, and `always` are defined.
|
|
472
|
+
* Those have `verify` method to check and report statements.
|
|
473
|
+
* @private
|
|
474
|
+
*/
|
|
475
|
+
const PaddingTypes = {
|
|
476
|
+
any: { verify: verifyForAny },
|
|
477
|
+
never: { verify: verifyForNever },
|
|
478
|
+
always: { verify: verifyForAlways },
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const MaybeMultilineStatementType: Record<string, NodeTestObject> = {
|
|
482
|
+
'block-like': { test: isBlockLikeStatement },
|
|
483
|
+
'expression': { test: isExpression },
|
|
484
|
+
'return': newKeywordTester('ReturnStatement', 'return'),
|
|
485
|
+
'export': newKeywordTester(
|
|
486
|
+
[
|
|
487
|
+
'ExportAllDeclaration',
|
|
488
|
+
'ExportDefaultDeclaration',
|
|
489
|
+
'ExportNamedDeclaration',
|
|
490
|
+
],
|
|
491
|
+
'export',
|
|
492
|
+
),
|
|
493
|
+
'var': newKeywordTester('VariableDeclaration', 'var'),
|
|
494
|
+
'let': newKeywordTester('VariableDeclaration', 'let'),
|
|
495
|
+
'const': newKeywordTester('VariableDeclaration', 'const'),
|
|
496
|
+
'using': {
|
|
497
|
+
test: node => node.type === 'VariableDeclaration'
|
|
498
|
+
&& (node.kind === 'using' || node.kind === 'await using'),
|
|
499
|
+
},
|
|
500
|
+
'type': newKeywordTester('TSTypeAliasDeclaration', 'type'),
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Types of statements.
|
|
505
|
+
* Those have `test` method to check it matches to the given statement.
|
|
506
|
+
* @private
|
|
507
|
+
*/
|
|
508
|
+
const StatementTypes: Record<string, NodeTestObject> = {
|
|
509
|
+
'*': { test: (): boolean => true },
|
|
510
|
+
'exports': { test: isCJSExport },
|
|
511
|
+
'require': { test: isCJSRequire },
|
|
512
|
+
'directive': { test: isDirectivePrologue },
|
|
513
|
+
'iife': { test: isIIFEStatement },
|
|
514
|
+
|
|
515
|
+
'block': newNodeTypeTester('BlockStatement'),
|
|
516
|
+
'empty': newNodeTypeTester('EmptyStatement'),
|
|
517
|
+
'function': newNodeTypeTester('FunctionDeclaration'),
|
|
518
|
+
'ts-method': newNodeTypeTester('TSMethodSignature'),
|
|
519
|
+
|
|
520
|
+
'break': newKeywordTester('BreakStatement', 'break'),
|
|
521
|
+
'case': newKeywordTester('SwitchCase', 'case'),
|
|
522
|
+
'class': newKeywordTester('ClassDeclaration', 'class'),
|
|
523
|
+
'continue': newKeywordTester('ContinueStatement', 'continue'),
|
|
524
|
+
'debugger': newKeywordTester('DebuggerStatement', 'debugger'),
|
|
525
|
+
'default': newKeywordTester(
|
|
526
|
+
['SwitchCase', 'ExportDefaultDeclaration'],
|
|
527
|
+
'default',
|
|
528
|
+
),
|
|
529
|
+
'do': newKeywordTester('DoWhileStatement', 'do'),
|
|
530
|
+
'for': newKeywordTester(
|
|
531
|
+
[
|
|
532
|
+
'ForStatement',
|
|
533
|
+
'ForInStatement',
|
|
534
|
+
'ForOfStatement',
|
|
535
|
+
],
|
|
536
|
+
'for',
|
|
537
|
+
),
|
|
538
|
+
'if': newKeywordTester('IfStatement', 'if'),
|
|
539
|
+
'import': newKeywordTester('ImportDeclaration', 'import'),
|
|
540
|
+
'switch': newKeywordTester('SwitchStatement', 'switch'),
|
|
541
|
+
'throw': newKeywordTester('ThrowStatement', 'throw'),
|
|
542
|
+
'try': newKeywordTester('TryStatement', 'try'),
|
|
543
|
+
'while': newKeywordTester(
|
|
544
|
+
['WhileStatement', 'DoWhileStatement'],
|
|
545
|
+
'while',
|
|
546
|
+
),
|
|
547
|
+
'with': newKeywordTester('WithStatement', 'with'),
|
|
548
|
+
|
|
549
|
+
'cjs-export': {
|
|
550
|
+
test: (node, sourceCode) => node.type === 'ExpressionStatement'
|
|
551
|
+
&& node.expression.type === 'AssignmentExpression'
|
|
552
|
+
&& CJS_EXPORT.test(sourceCode.getText(node.expression.left)),
|
|
553
|
+
},
|
|
554
|
+
'cjs-import': {
|
|
555
|
+
test: (node, sourceCode) => node.type === 'VariableDeclaration'
|
|
556
|
+
&& node.declarations.length > 0
|
|
557
|
+
&& node.declarations[0]?.init != null
|
|
558
|
+
&& CJS_IMPORT.test(sourceCode.getText(node.declarations[0].init)),
|
|
559
|
+
},
|
|
560
|
+
|
|
561
|
+
'enum': newKeywordTester(
|
|
562
|
+
'TSEnumDeclaration',
|
|
563
|
+
'enum',
|
|
564
|
+
),
|
|
565
|
+
'interface': newKeywordTester(
|
|
566
|
+
'TSInterfaceDeclaration',
|
|
567
|
+
'interface',
|
|
568
|
+
),
|
|
569
|
+
'function-overload': newNodeTypeTester('TSDeclareFunction'),
|
|
570
|
+
...Object.fromEntries(
|
|
571
|
+
Object.entries(MaybeMultilineStatementType)
|
|
572
|
+
.flatMap(([key, value]) => [
|
|
573
|
+
[key, value],
|
|
574
|
+
[
|
|
575
|
+
`singleline-${key}`,
|
|
576
|
+
{
|
|
577
|
+
...value,
|
|
578
|
+
test: (node, sourceCode) => value.test(node, sourceCode) && isSingleLine(node),
|
|
579
|
+
},
|
|
580
|
+
],
|
|
581
|
+
[
|
|
582
|
+
`multiline-${key}`,
|
|
583
|
+
{
|
|
584
|
+
...value,
|
|
585
|
+
test: (node, sourceCode) => value.test(node, sourceCode) && !isSingleLine(node),
|
|
586
|
+
},
|
|
587
|
+
],
|
|
588
|
+
]),
|
|
589
|
+
),
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/** Build the vendored padding rule with caller-owned, typed policy options. */
|
|
593
|
+
export default function createPaddingLineRule(options: RuleOptions): CreateRule {
|
|
594
|
+
return {
|
|
595
|
+
meta: {
|
|
596
|
+
type: 'layout',
|
|
597
|
+
docs: {
|
|
598
|
+
description: 'Require or disallow padding lines between statements',
|
|
599
|
+
},
|
|
600
|
+
fixable: 'whitespace',
|
|
601
|
+
hasSuggestions: false,
|
|
602
|
+
// This is intentionally an array schema as you can pass 0..n config objects
|
|
603
|
+
schema: {
|
|
604
|
+
$defs: {
|
|
605
|
+
paddingType: {
|
|
606
|
+
type: 'string',
|
|
607
|
+
enum: Object.keys(PaddingTypes),
|
|
608
|
+
},
|
|
609
|
+
statementType: {
|
|
610
|
+
type: 'string',
|
|
611
|
+
enum: Object.keys(StatementTypes),
|
|
612
|
+
},
|
|
613
|
+
selectorOption: {
|
|
614
|
+
type: 'object',
|
|
615
|
+
properties: {
|
|
616
|
+
selector: {
|
|
617
|
+
type: 'string',
|
|
618
|
+
},
|
|
619
|
+
lineMode: {
|
|
620
|
+
type: 'string',
|
|
621
|
+
enum: ['any', 'singleline', 'multiline'],
|
|
622
|
+
},
|
|
623
|
+
},
|
|
624
|
+
required: ['selector'],
|
|
625
|
+
additionalProperties: false,
|
|
626
|
+
},
|
|
627
|
+
statementMatcher: {
|
|
628
|
+
anyOf: [
|
|
629
|
+
{ $ref: '#/$defs/statementType' },
|
|
630
|
+
{ $ref: '#/$defs/selectorOption' },
|
|
631
|
+
],
|
|
632
|
+
},
|
|
633
|
+
statementOption: {
|
|
634
|
+
anyOf: [
|
|
635
|
+
{ $ref: '#/$defs/statementMatcher' },
|
|
636
|
+
{
|
|
637
|
+
type: 'array',
|
|
638
|
+
items: { $ref: '#/$defs/statementMatcher' },
|
|
639
|
+
minItems: 1,
|
|
640
|
+
uniqueItems: true,
|
|
641
|
+
additionalItems: false,
|
|
642
|
+
},
|
|
643
|
+
],
|
|
644
|
+
},
|
|
645
|
+
},
|
|
646
|
+
type: 'array',
|
|
647
|
+
additionalItems: false,
|
|
648
|
+
items: {
|
|
649
|
+
type: 'object',
|
|
650
|
+
properties: {
|
|
651
|
+
blankLine: { $ref: '#/$defs/paddingType' },
|
|
652
|
+
prev: { $ref: '#/$defs/statementOption' },
|
|
653
|
+
next: { $ref: '#/$defs/statementOption' },
|
|
654
|
+
},
|
|
655
|
+
additionalProperties: false,
|
|
656
|
+
required: ['blankLine', 'prev', 'next'],
|
|
657
|
+
},
|
|
658
|
+
},
|
|
659
|
+
messages: {
|
|
660
|
+
unexpectedBlankLine: 'Unexpected blank line before this statement.',
|
|
661
|
+
expectedBlankLine: 'Expected blank line before this statement.',
|
|
662
|
+
},
|
|
663
|
+
},
|
|
664
|
+
create(context) {
|
|
665
|
+
const sourceCode = context.sourceCode
|
|
666
|
+
|
|
667
|
+
const selectorMatchedNodes = new Map<string, Set<ASTNode>>()
|
|
668
|
+
const pendingPairs: { prevNode: ASTNode, nextNode: ASTNode }[] = []
|
|
669
|
+
|
|
670
|
+
function collectSelectorOption(option: StatementOption): void {
|
|
671
|
+
if (Array.isArray(option)) {
|
|
672
|
+
for (const item of option)
|
|
673
|
+
collectSelectorOption(item)
|
|
674
|
+
return
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
if (!isSelectorOption(option))
|
|
678
|
+
return
|
|
679
|
+
|
|
680
|
+
selectorMatchedNodes.set(option.selector, new Set())
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
for (const configure of options) {
|
|
684
|
+
collectSelectorOption(configure.prev)
|
|
685
|
+
collectSelectorOption(configure.next)
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
type Scope = {
|
|
689
|
+
upper: Scope
|
|
690
|
+
prevNode: ASTNode | null
|
|
691
|
+
} | null
|
|
692
|
+
|
|
693
|
+
let scopeInfo: Scope = null
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Processes to enter to new scope.
|
|
697
|
+
* This manages the current previous statement.
|
|
698
|
+
*
|
|
699
|
+
* @private
|
|
700
|
+
*/
|
|
701
|
+
function enterScope(): void {
|
|
702
|
+
scopeInfo = {
|
|
703
|
+
upper: scopeInfo,
|
|
704
|
+
prevNode: null,
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Processes to exit from the current scope.
|
|
710
|
+
*
|
|
711
|
+
* @private
|
|
712
|
+
*/
|
|
713
|
+
function exitScope(): void {
|
|
714
|
+
if (scopeInfo)
|
|
715
|
+
scopeInfo = scopeInfo.upper
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* Checks whether the given node matches the given type.
|
|
720
|
+
* @param node The statement node to check.
|
|
721
|
+
* @param type The statement type to check.
|
|
722
|
+
* @returns `true` if the statement node matched the type.
|
|
723
|
+
* @private
|
|
724
|
+
*/
|
|
725
|
+
function match(node: ASTNode, type: StatementOption): boolean {
|
|
726
|
+
let innerStatementNode = node
|
|
727
|
+
|
|
728
|
+
while (innerStatementNode.type === 'LabeledStatement')
|
|
729
|
+
innerStatementNode = innerStatementNode.body
|
|
730
|
+
|
|
731
|
+
if (Array.isArray(type))
|
|
732
|
+
return type.some(match.bind(null, innerStatementNode))
|
|
733
|
+
|
|
734
|
+
if (isSelectorOption(type)) {
|
|
735
|
+
const matchedNodes = selectorMatchedNodes.get(type.selector)
|
|
736
|
+
if (!matchedNodes?.has(innerStatementNode))
|
|
737
|
+
return false
|
|
738
|
+
|
|
739
|
+
const lineMode = type.lineMode
|
|
740
|
+
|
|
741
|
+
if (lineMode === 'singleline')
|
|
742
|
+
return isSingleLine(innerStatementNode)
|
|
743
|
+
else if (lineMode === 'multiline')
|
|
744
|
+
return !isSingleLine(innerStatementNode)
|
|
745
|
+
|
|
746
|
+
return true
|
|
747
|
+
}
|
|
748
|
+
else {
|
|
749
|
+
const statementType = StatementTypes[type]
|
|
750
|
+
if (statementType === undefined)
|
|
751
|
+
throw new Error(`Padding rule invariant: unsupported statement type ${type}`)
|
|
752
|
+
return statementType.test(innerStatementNode, sourceCode)
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Finds the last matched configure from options.
|
|
758
|
+
* @param prevNode The previous statement to match.
|
|
759
|
+
* @param nextNode The current statement to match.
|
|
760
|
+
* @returns The tester of the last matched configure.
|
|
761
|
+
* @private
|
|
762
|
+
*/
|
|
763
|
+
function getPaddingType(
|
|
764
|
+
prevNode: ASTNode,
|
|
765
|
+
nextNode: ASTNode,
|
|
766
|
+
): (typeof PaddingTypes)[keyof typeof PaddingTypes] {
|
|
767
|
+
for (let i = options.length - 1; i >= 0; --i) {
|
|
768
|
+
const configure = options[i]
|
|
769
|
+
if (configure === undefined)
|
|
770
|
+
throw new Error('Padding rule invariant: configuration entry is missing')
|
|
771
|
+
if (
|
|
772
|
+
match(prevNode, configure.prev)
|
|
773
|
+
&& match(nextNode, configure.next)
|
|
774
|
+
) {
|
|
775
|
+
return PaddingTypes[configure.blankLine]
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
return PaddingTypes.any
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* Gets padding line sequences between the given 2 statements.
|
|
783
|
+
* Comments are separators of the padding line sequences.
|
|
784
|
+
* @param prevNode The previous statement to count.
|
|
785
|
+
* @param nextNode The current statement to count.
|
|
786
|
+
* @returns The array of token pairs.
|
|
787
|
+
* @private
|
|
788
|
+
*/
|
|
789
|
+
function getPaddingLineSequences(
|
|
790
|
+
prevNode: ASTNode,
|
|
791
|
+
nextNode: ASTNode,
|
|
792
|
+
): [Token, Token][] {
|
|
793
|
+
const pairs: [Token, Token][] = []
|
|
794
|
+
let prevToken: Token = getActualLastToken(prevNode, sourceCode)!
|
|
795
|
+
|
|
796
|
+
if (nextNode.loc.start.line - prevToken.loc.end.line >= 2) {
|
|
797
|
+
do {
|
|
798
|
+
const token: Token = sourceCode.getTokenAfter(prevToken, {
|
|
799
|
+
includeComments: true,
|
|
800
|
+
})!
|
|
801
|
+
|
|
802
|
+
if (token.loc.start.line - prevToken.loc.end.line >= 2)
|
|
803
|
+
pairs.push([prevToken, token])
|
|
804
|
+
|
|
805
|
+
prevToken = token
|
|
806
|
+
} while (prevToken.range[0] < nextNode.range[0])
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
return pairs
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* Verify padding lines between the given node and the previous node.
|
|
814
|
+
* @param node The node to verify.
|
|
815
|
+
*
|
|
816
|
+
* @private
|
|
817
|
+
*/
|
|
818
|
+
function verify(node: ASTNode): void {
|
|
819
|
+
if (
|
|
820
|
+
!node.parent
|
|
821
|
+
|| ![
|
|
822
|
+
'BlockStatement',
|
|
823
|
+
'Program',
|
|
824
|
+
'StaticBlock',
|
|
825
|
+
'SwitchCase',
|
|
826
|
+
'SwitchStatement',
|
|
827
|
+
'TSInterfaceBody',
|
|
828
|
+
'TSModuleBlock',
|
|
829
|
+
'TSTypeLiteral',
|
|
830
|
+
].includes(node.parent.type)
|
|
831
|
+
) {
|
|
832
|
+
return
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// Save this node as the current previous statement.
|
|
836
|
+
const prevNode = scopeInfo!.prevNode
|
|
837
|
+
|
|
838
|
+
// Verify.
|
|
839
|
+
if (prevNode)
|
|
840
|
+
pendingPairs.push({ prevNode, nextNode: node })
|
|
841
|
+
|
|
842
|
+
scopeInfo!.prevNode = node
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
function verifyPendingPairs(): void {
|
|
846
|
+
for (const { prevNode, nextNode } of pendingPairs) {
|
|
847
|
+
const type = getPaddingType(prevNode, nextNode)
|
|
848
|
+
const paddingLines = getPaddingLineSequences(prevNode, nextNode)
|
|
849
|
+
|
|
850
|
+
type.verify(context, prevNode, nextNode, paddingLines)
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/**
|
|
855
|
+
* Verify padding lines between the given node and the previous node.
|
|
856
|
+
* Then process to enter to new scope.
|
|
857
|
+
* @param node The node to verify.
|
|
858
|
+
*
|
|
859
|
+
* @private
|
|
860
|
+
*/
|
|
861
|
+
function verifyThenEnterScope(node: ASTNode): void {
|
|
862
|
+
verify(node)
|
|
863
|
+
enterScope()
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
const selectorMatchListeners = Object.fromEntries(
|
|
867
|
+
Array.from(selectorMatchedNodes.keys(), selector => [
|
|
868
|
+
selector,
|
|
869
|
+
(node: ASTNode): void => {
|
|
870
|
+
selectorMatchedNodes.get(selector)?.add(node)
|
|
871
|
+
},
|
|
872
|
+
]),
|
|
873
|
+
)
|
|
874
|
+
|
|
875
|
+
return {
|
|
876
|
+
'Program': enterScope,
|
|
877
|
+
'Program:exit': () => {
|
|
878
|
+
verifyPendingPairs()
|
|
879
|
+
exitScope()
|
|
880
|
+
},
|
|
881
|
+
'BlockStatement': enterScope,
|
|
882
|
+
'BlockStatement:exit': exitScope,
|
|
883
|
+
'SwitchStatement': enterScope,
|
|
884
|
+
'SwitchStatement:exit': exitScope,
|
|
885
|
+
'SwitchCase': verifyThenEnterScope,
|
|
886
|
+
'SwitchCase:exit': exitScope,
|
|
887
|
+
'StaticBlock': enterScope,
|
|
888
|
+
'StaticBlock:exit': exitScope,
|
|
889
|
+
|
|
890
|
+
'TSInterfaceBody': enterScope,
|
|
891
|
+
'TSInterfaceBody:exit': exitScope,
|
|
892
|
+
'TSModuleBlock': enterScope,
|
|
893
|
+
'TSModuleBlock:exit': exitScope,
|
|
894
|
+
'TSTypeLiteral': enterScope,
|
|
895
|
+
'TSTypeLiteral:exit': exitScope,
|
|
896
|
+
'TSDeclareFunction': verifyThenEnterScope,
|
|
897
|
+
'TSDeclareFunction:exit': exitScope,
|
|
898
|
+
'TSMethodSignature': verifyThenEnterScope,
|
|
899
|
+
'TSMethodSignature:exit': exitScope,
|
|
900
|
+
|
|
901
|
+
':statement': verify,
|
|
902
|
+
...selectorMatchListeners,
|
|
903
|
+
}
|
|
904
|
+
},
|
|
905
|
+
}
|
|
906
|
+
}
|