@timmo001/oxlint-rules 0.3.1 → 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.
Files changed (25) hide show
  1. package/README.md +2 -1
  2. package/THIRD_PARTY_NOTICES.md +9 -0
  3. package/dist/cli.js +733 -35
  4. package/dist/configs/recommended-effect.js +767 -69
  5. package/dist/configs/recommended.js +535 -30
  6. package/dist/upstream/anti-slop.js +535 -30
  7. package/dist/upstream/effect.js +196 -3
  8. package/package.json +1 -1
  9. package/vendor/anti-slop/src/effect/index.ts +8 -0
  10. package/vendor/anti-slop/src/effect/rules/no-manual-effect-error-tag.ts +52 -0
  11. package/vendor/anti-slop/src/effect/rules/no-manual-tag-comparison.ts +45 -0
  12. package/vendor/anti-slop/src/effect/rules/no-manual-tagged-construction.ts +37 -0
  13. package/vendor/anti-slop/src/effect/rules/prefer-effect-match.ts +54 -0
  14. package/vendor/anti-slop/src/effect/shared/tagged-values.ts +97 -0
  15. package/vendor/anti-slop/src/index.ts +2 -0
  16. package/vendor/anti-slop/src/rules/no-known-value-widening.ts +2 -14
  17. package/vendor/anti-slop/src/rules/no-module-mocking.ts +3 -14
  18. package/vendor/anti-slop/src/rules/require-readable-spacing.ts +47 -0
  19. package/vendor/anti-slop/src/shared/reflect-method.ts +2 -13
  20. package/vendor/anti-slop/src/shared/scope.ts +15 -0
  21. package/vendor/anti-slop/src/vendor/eslint-stylistic/LICENSE +22 -0
  22. package/vendor/anti-slop/src/vendor/eslint-stylistic/UPSTREAM.md +28 -0
  23. package/vendor/anti-slop/src/vendor/eslint-stylistic/padding-line-ast.ts +51 -0
  24. package/vendor/anti-slop/src/vendor/eslint-stylistic/padding-line-between-statements.ts +906 -0
  25. 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
+ }