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