@tuomashatakka/eslint-config 2.6.2 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/.github/workflows/ci.yml +23 -0
  2. package/.github/workflows/publish.yml +45 -0
  3. package/AGENTS.md +29 -0
  4. package/bun.lock +60 -102
  5. package/eslint.config.mjs +1 -0
  6. package/index.mjs +7 -21
  7. package/package.json +11 -19
  8. package/plugins/no-inline-types/index.mjs +11 -0
  9. package/plugins/no-inline-types/rules/no-inline-multiline-types.mjs +181 -0
  10. package/plugins/omit/index.mjs +8 -0
  11. package/plugins/omit/rules/omit-unnecessary-parens-brackets.mjs +329 -0
  12. package/plugins/omit/utils.mjs +91 -0
  13. package/plugins/react-strict/index.mjs +19 -0
  14. package/plugins/react-strict/rules/jsx-prop-layout.mjs +100 -0
  15. package/plugins/react-strict/rules/no-complex-jsx-map.mjs +66 -0
  16. package/plugins/react-strict/rules/no-jsx-value-calculations.mjs +99 -0
  17. package/plugins/react-strict/rules/no-nested-divs.mjs +59 -0
  18. package/plugins/react-strict/rules/no-style-prop.mjs +43 -0
  19. package/plugins/react-strict/rules/prefer-no-use-effect.mjs +26 -0
  20. package/plugins/whitespaced/index.mjs +15 -0
  21. package/plugins/whitespaced/rules/aligned-assignments.mjs +385 -0
  22. package/plugins/whitespaced/rules/block-padding.mjs +289 -0
  23. package/plugins/whitespaced/rules/class-property-grouping.mjs +370 -0
  24. package/plugins/whitespaced/rules/consistent-line-spacing.mjs +266 -0
  25. package/plugins/whitespaced/rules/multiline-format.mjs +533 -0
  26. package/rules.mjs +101 -95
  27. package/test/fixtures/basic-javascript.js +5 -4
  28. package/test/fixtures/complex-patterns.ts +9 -7
  29. package/test/fixtures/edge-cases.js +12 -7
  30. package/test/fixtures/jsx-formatting.jsx +5 -4
  31. package/test/fixtures/omit-parens.invalid.ts +12 -0
  32. package/test/fixtures/omit-parens.valid.ts +13 -0
  33. package/test/fixtures/react-component.tsx +7 -6
  34. package/test/fixtures/react-strict.invalid.tsx +31 -0
  35. package/test/fixtures/react-strict.valid.tsx +76 -0
  36. package/test/fixtures/whitespaced-docstring.invalid.ts +10 -0
  37. package/test/fixtures/whitespaced-docstring.valid.ts +16 -0
  38. package/test/fixtures/whitespaced-members.invalid.ts +22 -0
  39. package/test/fixtures/whitespaced-members.valid.ts +13 -0
  40. package/test/fixtures/whitespaced-multiline.invalid.ts +8 -0
  41. package/test/fixtures/whitespaced-multiline.valid.ts +15 -0
  42. package/test/fixtures/whitespaced-types.valid.ts +5 -0
  43. package/test/fixtures/whitespaced.valid.ts +45 -0
  44. package/test/format-cases.mjs +13 -14
  45. package/test/test-runner.mjs +128 -47
@@ -0,0 +1,385 @@
1
+ /**
2
+ * @fileoverview ESLint rule to enforce vertically aligned assignments
3
+ * @author tuomashatakka
4
+ */
5
+
6
+ export default {
7
+ meta: {
8
+ type: 'layout',
9
+ docs: {
10
+ description: 'Enforce vertically aligned assignments in declaration blocks',
11
+ category: 'Stylistic Issues',
12
+ recommended: false,
13
+ },
14
+ fixable: 'whitespace',
15
+ schema: [{
16
+ type: 'object',
17
+ properties: {
18
+ alignComments: { type: 'boolean', default: false },
19
+ alignLiterals: { type: 'boolean', default: false },
20
+ blockSize: { type: 'integer', minimum: 2, default: 2 },
21
+ ignoreAdjacent: { type: 'boolean', default: true },
22
+ ignoreIfAssignmentsNotInBlock: { type: 'boolean', default: true },
23
+ alignTypes: { type: 'boolean', default: false },
24
+ ignoreTypesMismatch: { type: 'boolean', default: true },
25
+ alignMemberAssignments: { type: 'boolean', default: true },
26
+ },
27
+ additionalProperties: false,
28
+ }],
29
+ messages: {
30
+ misalignedAssignment: 'Assignment operators should be vertically aligned within blocks.',
31
+ misalignedTypes: 'Type declarations should be vertically aligned within blocks.',
32
+ },
33
+ },
34
+ create (context) {
35
+ const sourceCode = context.sourceCode || context.getSourceCode()
36
+ const options = context.options[0] || {}
37
+
38
+ const alignComments = options.alignComments !== undefined ? options.alignComments : false
39
+ const alignLiterals = options.alignLiterals !== undefined ? options.alignLiterals : false
40
+ const blockSize = options.blockSize !== undefined ? options.blockSize : 2
41
+ const ignoreAdjacent = options.ignoreAdjacent !== undefined ? options.ignoreAdjacent : true
42
+ const ignoreIfAssignmentsNotInBlock = options.ignoreIfAssignmentsNotInBlock !== undefined ? options.ignoreIfAssignmentsNotInBlock : true
43
+ const alignTypes = options.alignTypes !== undefined ? options.alignTypes : false
44
+ const ignoreTypesMismatch = options.ignoreTypesMismatch !== undefined ? options.ignoreTypesMismatch : true
45
+ const alignMemberAssignments = options.alignMemberAssignments !== undefined ? options.alignMemberAssignments : true
46
+
47
+
48
+ function getEqualsColumn (declarator) {
49
+ const equalsToken = sourceCode.getTokenBefore(
50
+ declarator.init,
51
+ token => token.value === '='
52
+ )
53
+
54
+ return equalsToken ? equalsToken.loc.start.column : null
55
+ }
56
+
57
+
58
+ function getTypeColonColumn (declarator) {
59
+ if (declarator.id && declarator.id.typeAnnotation) {
60
+ const colonToken = sourceCode.getFirstToken(declarator.id.typeAnnotation)
61
+ return colonToken ? colonToken.loc.start.column : null
62
+ }
63
+ return null
64
+ }
65
+
66
+
67
+ function areNodesAdjacent (node1, node2) {
68
+ return node2.loc.start.line === node1.loc.end.line + 1
69
+ }
70
+
71
+
72
+ function haveSameKind (declarations) {
73
+ if (!declarations.length)
74
+ return true
75
+
76
+ const firstKind = declarations[0].parent.kind
77
+ return declarations.every(decl => decl.parent.kind === firstKind)
78
+ }
79
+
80
+
81
+ function allHaveTypes (declarations) {
82
+ return declarations.every(decl =>
83
+ decl.id && decl.id.typeAnnotation
84
+ )
85
+ }
86
+
87
+
88
+ function anyHaveTypes (declarations) {
89
+ return declarations.some(decl =>
90
+ decl.id && decl.id.typeAnnotation
91
+ )
92
+ }
93
+
94
+
95
+ function getMaxEqualsColumn (declarations) {
96
+ return Math.max(...declarations.map(getEqualsColumn))
97
+ }
98
+
99
+
100
+ function getMaxTypeColonColumn (declarations) {
101
+ const columns = declarations
102
+ .map(getTypeColonColumn)
103
+ .filter(column => column !== null)
104
+
105
+ return columns.length ? Math.max(...columns) : null
106
+ }
107
+
108
+
109
+ function getFixedDeclaration (declarator, targetMaxEqualsColumn) {
110
+ const originalText = sourceCode.getText(declarator)
111
+ const idText = sourceCode.getText(declarator.id)
112
+ const initText = declarator.init ? sourceCode.getText(declarator.init) : ''
113
+
114
+ if (!initText)
115
+ return originalText
116
+
117
+ // Get the equals token
118
+ const equalsToken = sourceCode.getTokenBefore(
119
+ declarator.init,
120
+ token => token.value === '='
121
+ )
122
+
123
+ if (!equalsToken)
124
+ return originalText
125
+
126
+ const currentEqualsColumn = equalsToken.loc.start.column
127
+ const padding = targetMaxEqualsColumn - currentEqualsColumn
128
+
129
+ if (padding <= 0)
130
+ return originalText
131
+
132
+ // Build the fixed text: id + padding + '= ' + init
133
+ const result = idText + ' '.repeat(padding) + '= ' + initText
134
+
135
+ return result
136
+ }
137
+
138
+
139
+ function getIdLengthWithType (declarator) {
140
+ const idText = sourceCode.getText(declarator.id)
141
+ if (declarator.id && declarator.id.typeAnnotation) {
142
+ const typeText = sourceCode.getText(declarator.id.typeAnnotation)
143
+ return idText.length + 1 + typeText.length
144
+ }
145
+ return idText.length
146
+ }
147
+
148
+
149
+ function checkAlignment (declarations) {
150
+ if (declarations.length < blockSize)
151
+ return
152
+
153
+ if (ignoreIfAssignmentsNotInBlock && !haveSameKind(declarations))
154
+ return
155
+
156
+ // Get actual equals column positions
157
+ const equalsColumns = declarations.map(d => getEqualsColumn(d)).filter(c => c !== null)
158
+
159
+ // Check if already aligned by comparing equals columns directly
160
+ if (equalsColumns.length >= 2) {
161
+ const maxEqualsCol = Math.max(...equalsColumns)
162
+ const minEqualsCol = Math.min(...equalsColumns)
163
+ if (maxEqualsCol === minEqualsCol)
164
+ return
165
+ }
166
+
167
+ const maxIdLength = Math.max(...declarations.map(getIdLengthWithType))
168
+
169
+ let maxTypeLength = null
170
+ if (alignTypes && anyHaveTypes(declarations)) {
171
+ if (ignoreTypesMismatch && !allHaveTypes(declarations)) {
172
+ // Skip only type alignment but still do equals alignment
173
+ }
174
+ else {
175
+ const typeLengths = declarations
176
+ .map(d => d.id?.typeAnnotation ? sourceCode.getText(d.id.typeAnnotation).length : 0)
177
+ .filter(l => l > 0)
178
+ maxTypeLength = typeLengths.length ? Math.max(...typeLengths) : null
179
+ }
180
+ }
181
+
182
+ const maxEqualsColumn = Math.max(...equalsColumns)
183
+
184
+ declarations.forEach(declarator => {
185
+ const equalsCol = getEqualsColumn(declarator)
186
+
187
+ if (equalsCol !== null && equalsCol !== maxEqualsColumn)
188
+ context.report({
189
+ node: declarator,
190
+ messageId: 'misalignedAssignment',
191
+ fix (fixer) {
192
+ return fixer.replaceText(
193
+ declarator,
194
+ getFixedDeclaration(declarator, maxEqualsColumn)
195
+ )
196
+ },
197
+ })
198
+ })
199
+ }
200
+
201
+
202
+ function processDeclarationGroup (declarations) {
203
+ if (!declarations.length)
204
+ return
205
+
206
+ const declarationsWithInits = declarations.filter(decl => decl.init)
207
+
208
+ if (declarationsWithInits.length < blockSize)
209
+ return
210
+
211
+ if (ignoreAdjacent) {
212
+ const adjacentGroups = []
213
+ let currentGroup = [ declarationsWithInits[0] ]
214
+
215
+ for (let i = 1; i < declarationsWithInits.length; i++) {
216
+ const prevDecl = declarationsWithInits[i - 1]
217
+ const currentDecl = declarationsWithInits[i]
218
+
219
+ if (areNodesAdjacent(prevDecl, currentDecl))
220
+ currentGroup.push(currentDecl); else {
221
+ if (currentGroup.length >= blockSize)
222
+ adjacentGroups.push(currentGroup)
223
+ currentGroup = [ currentDecl ]
224
+ }
225
+ }
226
+
227
+ if (currentGroup.length >= blockSize)
228
+ adjacentGroups.push(currentGroup)
229
+
230
+ adjacentGroups.forEach(checkAlignment)
231
+ }
232
+ else
233
+ checkAlignment(declarationsWithInits)
234
+ }
235
+
236
+
237
+ // ── Member assignment alignment (this.prop = value, obj.prop = value) ────
238
+
239
+ function getMemberAssignEqualsColumn (exprStmt) {
240
+ const assignExpr = exprStmt.expression
241
+ const equalsToken = sourceCode.getTokenBefore(
242
+ assignExpr.right,
243
+ token => token.value === '=' && token.type === 'Punctuator'
244
+ )
245
+ return equalsToken ? equalsToken.loc.start.column : null
246
+ }
247
+
248
+
249
+ function getFixedMemberAssignment (exprStmt, targetMaxLeftLength) {
250
+ const assignExpr = exprStmt.expression
251
+ const left = assignExpr.left
252
+ const right = assignExpr.right
253
+ const leftText = sourceCode.getText(left)
254
+ const rightText = sourceCode.getText(right)
255
+
256
+ const leftLength = leftText.length
257
+ const padding = targetMaxLeftLength - leftLength
258
+
259
+ return leftText + ' '.repeat(padding) + '= ' + rightText
260
+ }
261
+
262
+
263
+ function getMemberLeftLength (exprStmt) {
264
+ const left = exprStmt.expression.left
265
+ return sourceCode.getText(left).trimEnd().length
266
+ }
267
+
268
+
269
+ function getMemberEqualsColumn (exprStmt) {
270
+ const assignExpr = exprStmt.expression
271
+ const equalsToken = sourceCode.getTokenBefore(
272
+ assignExpr.right,
273
+ token => token.value === '=' && token.type === 'Punctuator'
274
+ )
275
+ return equalsToken ? equalsToken.loc.start.column : null
276
+ }
277
+
278
+
279
+ function checkMemberAlignment (stmts) {
280
+ if (stmts.length < blockSize)
281
+ return
282
+
283
+ const lengths = stmts.map(getMemberLeftLength)
284
+ const maxLength = Math.max(...lengths)
285
+
286
+ // Check if already aligned by comparing equals columns
287
+ const equalsColumns = stmts.map(s => getMemberEqualsColumn(s)).filter(c => c !== null)
288
+ if (equalsColumns.length >= 2) {
289
+ const maxCol = Math.max(...equalsColumns)
290
+ const allAligned = equalsColumns.every(c => c === maxCol)
291
+ if (allAligned)
292
+ return
293
+ }
294
+
295
+ stmts.forEach(stmt => {
296
+ const length = getMemberLeftLength(stmt)
297
+ if (length !== maxLength)
298
+ context.report({
299
+ node: stmt.expression,
300
+ messageId: 'misalignedAssignment',
301
+ fix (fixer) {
302
+ return fixer.replaceText(stmt.expression, getFixedMemberAssignment(stmt, maxLength))
303
+ },
304
+ })
305
+ })
306
+ }
307
+
308
+
309
+ function processMemberAssignments (blockBody) {
310
+ if (!alignMemberAssignments)
311
+ return
312
+
313
+ const memberStmts = blockBody.filter(stmt =>
314
+ stmt.type === 'ExpressionStatement' &&
315
+ stmt.expression &&
316
+ stmt.expression.type === 'AssignmentExpression' &&
317
+ stmt.expression.operator === '=' &&
318
+ stmt.expression.left.type === 'MemberExpression'
319
+ )
320
+
321
+ if (memberStmts.length < blockSize)
322
+ return
323
+
324
+ if (ignoreAdjacent) {
325
+ const groups = []
326
+ let group = [ memberStmts[0] ]
327
+
328
+ for (let i = 1; i < memberStmts.length; i++)
329
+ if (areNodesAdjacent(memberStmts[i - 1], memberStmts[i]))
330
+ group.push(memberStmts[i])
331
+ else {
332
+ if (group.length >= blockSize)
333
+ groups.push(group)
334
+ group = [ memberStmts[i] ]
335
+ }
336
+ if (group.length >= blockSize)
337
+ groups.push(group)
338
+ groups.forEach(checkMemberAlignment)
339
+ }
340
+ else
341
+ checkMemberAlignment(memberStmts)
342
+ }
343
+
344
+
345
+ // ── Shared block processor ─────────────────────────────────────────────
346
+
347
+ function processBlockVariables (node) {
348
+ const scopeBody = node.type === 'Program' ? node.body : node.body ? node.body : []
349
+ const declarations = []
350
+
351
+ for (const statement of scopeBody)
352
+ if (statement.type === 'VariableDeclaration')
353
+ declarations.push(...statement.declarations)
354
+
355
+ processDeclarationGroup(declarations)
356
+ processMemberAssignments(scopeBody)
357
+ }
358
+
359
+
360
+ return {
361
+ Program (node) {
362
+ processBlockVariables(node)
363
+ },
364
+
365
+ BlockStatement (node) {
366
+ processBlockVariables(node)
367
+ },
368
+
369
+ SwitchCase (node) {
370
+ if (!node.consequent)
371
+ return
372
+
373
+ const declarations = []
374
+ const memberStmts = []
375
+
376
+ for (const statement of node.consequent)
377
+ if (statement.type === 'VariableDeclaration')
378
+ declarations.push(...statement.declarations)
379
+
380
+ processDeclarationGroup(declarations)
381
+ processMemberAssignments(node.consequent)
382
+ },
383
+ }
384
+ },
385
+ }
@@ -0,0 +1,289 @@
1
+ /**
2
+ * @fileoverview ESLint rule to enforce whitespaced block padding with docstring support
3
+ * @author tuomashatakka
4
+ */
5
+
6
+ export default {
7
+ meta: {
8
+ type: 'layout',
9
+ docs: {
10
+ description: 'Enforce whitespaced block padding with docstring support',
11
+ category: "Stylistic Issues",
12
+ recommended: false,
13
+ },
14
+ fixable: 'whitespace',
15
+ schema: [{
16
+ type: "object",
17
+ properties: {
18
+ rootBlockPadding: { type: 'integer', minimum: 0, default: 2 },
19
+ nestedBlockPadding: { type: 'integer', minimum: 0, default: 1 },
20
+ enforceBeginningPadding: { type: 'boolean', default: false },
21
+ enforceEndPadding: { type: 'boolean', default: false },
22
+ docstringPadding: { type: 'integer', minimum: 0, default: 1 },
23
+ treatCommentsAsDocstrings: { type: 'boolean', default: true },
24
+ },
25
+ additionalProperties: false,
26
+ },],
27
+ messages: {
28
+ missingPaddingBetweenRootBlocks: "Expected {{expected}} empty {{lineText}} between root-level blocks, but found {{actual}}.",
29
+ missingPaddingBetweenNestedBlocks: 'Expected {{expected}} empty {{lineText}} between nested blocks, but found {{actual}}.',
30
+ missingPaddingAtBeginning: "Expected no empty lines at the beginning of the file.",
31
+ missingPaddingAtEnd: "Expected {{expected}} empty {{lineText}} at the end of the file, but found {{actual}}.",
32
+ missingPaddingAfterDocstring: "Expected {{expected}} empty {{lineText}} after docstring, but found {{actual}}.",
33
+ },
34
+ },
35
+ create (context) {
36
+ const sourceCode = context.sourceCode || context.getSourceCode()
37
+ const options = context.options[0] || {}
38
+ const rootBlockPadding = options.rootBlockPadding !== undefined ? options.rootBlockPadding : 2
39
+ const nestedBlockPadding = options.nestedBlockPadding !== undefined ? options.nestedBlockPadding : 1
40
+ const enforceBeginningPadding = options.enforceBeginningPadding !== undefined ? options.enforceBeginningPadding : false
41
+ const enforceEndPadding = options.enforceEndPadding !== undefined ? options.enforceEndPadding : false
42
+ const docstringPadding = options.docstringPadding !== undefined ? options.docstringPadding : 1
43
+ const treatCommentsAsDocstrings = options.treatCommentsAsDocstrings !== undefined ? options.treatCommentsAsDocstrings : true
44
+
45
+ function getBlankLinesBetween (node1, node2) {
46
+ const node1End = node1.loc.end.line
47
+ const node2Start = node2.loc.start.line
48
+ return node2Start - node1End - 1
49
+ }
50
+
51
+
52
+ function isDocstring (comment) {
53
+ if (comment.type === 'Block') {
54
+ const commentText = comment.value
55
+ const isMultiline = commentText.includes('\n');
56
+ const hasDocstringPatterns = (/\s*@\w+|param|return|description|example|author/).test(commentText)
57
+ return isMultiline || hasDocstringPatterns
58
+ }
59
+ return false
60
+ }
61
+
62
+
63
+ function getDocstringComments (node) {
64
+ const comments = sourceCode.getCommentsBefore(node)
65
+
66
+ if (!comments || !comments.length)
67
+ return []
68
+
69
+ if (comments.some(comment => comment.type === 'Block')) {
70
+ const lastBlockComment = [ ...comments ].reverse().find(comment => comment.type === 'Block')
71
+ return lastBlockComment && isDocstring(lastBlockComment)
72
+ ? [
73
+ lastBlockComment,
74
+ ]
75
+ : []
76
+ }
77
+
78
+ if (treatCommentsAsDocstrings && comments.some(comment => comment.type === 'Line')) {
79
+ const consecutiveLineComments = []
80
+ let lastLine = -1
81
+
82
+ for (let i = comments.length - 1; i >= 0; i--) {
83
+ const comment = comments[i]
84
+ if (comment.type !== 'Line')
85
+ continue;
86
+
87
+ if (lastLine === -1 || comment.loc.end.line + 1 === lastLine) {
88
+ consecutiveLineComments.unshift(comment)
89
+ lastLine = comment.loc.start.line
90
+ }
91
+ else
92
+ break;
93
+ }
94
+
95
+ if (consecutiveLineComments.length >= 2 ||
96
+ consecutiveLineComments.length === 1 &&
97
+ (/\s*@\w+|param|return|description|example|author/).test(consecutiveLineComments[0].value))
98
+ return consecutiveLineComments
99
+ }
100
+
101
+ return []
102
+ }
103
+
104
+
105
+ function checkDocstringPadding (node) {
106
+ const docstringComments = getDocstringComments(node)
107
+
108
+ if (!docstringComments.length)
109
+ return;
110
+
111
+ const lastComment = docstringComments[docstringComments.length - 1]
112
+ const commentEnd = lastComment.loc.end.line
113
+ const nodeStart = node.loc.start.line
114
+ const blankLines = nodeStart - commentEnd - 1
115
+
116
+ if (blankLines !== docstringPadding)
117
+ context.report({
118
+ node,
119
+ messageId: "missingPaddingAfterDocstring",
120
+ data: {
121
+ expected: docstringPadding,
122
+ actual: blankLines,
123
+ lineText: docstringPadding === 1 ? "line" : "lines",
124
+ },
125
+ fix(fixer) {
126
+ const range = [lastComment.range[1], sourceCode.getFirstToken(node).range[0]];
127
+ const newLines = "\n".repeat(docstringPadding + 1);
128
+ return fixer.replaceTextRange(range, newLines);
129
+ },
130
+ });
131
+ }
132
+
133
+
134
+ function checkRootLevelBlankLines (nodes) {
135
+ for (let i = 0; i < nodes.length - 1; i++) {
136
+ const currentNode = nodes[i]
137
+ const nextNode = nodes[i + 1]
138
+ const blankLines = getBlankLinesBetween(currentNode, nextNode)
139
+
140
+ if (blankLines !== rootBlockPadding)
141
+ context.report({
142
+ node: nextNode,
143
+ messageId: "missingPaddingBetweenRootBlocks",
144
+ data: {
145
+ expected: rootBlockPadding,
146
+ actual: blankLines,
147
+ lineText: rootBlockPadding === 1 ? "line" : "lines",
148
+ },
149
+ fix(fixer) {
150
+ const endOfCurrentNode = sourceCode.getLastToken(currentNode);
151
+ const startOfNextNode = sourceCode.getFirstToken(nextNode);
152
+ const range = [endOfCurrentNode.range[1], startOfNextNode.range[0]];
153
+ const newLines = "\n".repeat(rootBlockPadding + 1);
154
+ return fixer.replaceTextRange(range, newLines);
155
+ },
156
+ });
157
+ }
158
+ }
159
+
160
+
161
+ function checkNestedBlankLines (node, bodyNodes) {
162
+ for (let i = 0; i < bodyNodes.length - 1; i++) {
163
+ const currentNode = bodyNodes[i]
164
+ const nextNode = bodyNodes[i + 1]
165
+ const blankLines = getBlankLinesBetween(currentNode, nextNode)
166
+
167
+ if (blankLines !== nestedBlockPadding)
168
+ context.report({
169
+ node: nextNode,
170
+ messageId: "missingPaddingBetweenNestedBlocks",
171
+ data: {
172
+ expected: nestedBlockPadding,
173
+ actual: blankLines,
174
+ lineText: nestedBlockPadding === 1 ? "line" : "lines",
175
+ },
176
+ fix(fixer) {
177
+ const endOfCurrentNode = sourceCode.getLastToken(currentNode);
178
+ const startOfNextNode = sourceCode.getFirstToken(nextNode);
179
+ const range = [endOfCurrentNode.range[1], startOfNextNode.range[0]];
180
+ const newLines = "\n".repeat(nestedBlockPadding + 1);
181
+ return fixer.replaceTextRange(range, newLines);
182
+ },
183
+ });
184
+ }
185
+ }
186
+
187
+
188
+ function checkBeginningPadding (firstNode) {
189
+ if (enforceBeginningPadding && firstNode) {
190
+ const startLine = firstNode.loc.start.line
191
+
192
+ if (startLine > 1)
193
+ context.report({
194
+ node: firstNode,
195
+ messageId: "missingPaddingAtBeginning",
196
+ fix(fixer) {
197
+ const rangeStart = 0;
198
+ const rangeEnd = sourceCode.getFirstToken(firstNode).range[0];
199
+ const sourceText = sourceCode.getText().substring(0, rangeEnd);
200
+ const contentStart = sourceText.search(/\S/);
201
+
202
+ if (contentStart !== -1) {
203
+ return fixer.removeRange([0, contentStart]);
204
+ } else {
205
+ return fixer.removeRange([0, rangeEnd]);
206
+ }
207
+ },
208
+ });
209
+ }
210
+ }
211
+
212
+
213
+ function checkEndPadding (lastNode) {
214
+ if (enforceEndPadding && lastNode) {
215
+ const sourceText = sourceCode.getText()
216
+ const lastNodeEnd = lastNode.loc.end.line
217
+ const totalLines = sourceText.split('\n').length
218
+ const blankLinesAtEnd = totalLines - lastNodeEnd
219
+
220
+ if (blankLinesAtEnd !== rootBlockPadding)
221
+ context.report({
222
+ node: lastNode,
223
+ messageId: "missingPaddingAtEnd",
224
+ data: {
225
+ expected: rootBlockPadding,
226
+ actual: blankLinesAtEnd,
227
+ lineText: rootBlockPadding === 1 ? "line" : "lines",
228
+ },
229
+ fix(fixer) {
230
+ const endOfLastNode = sourceCode.getLastToken(lastNode);
231
+ const end = sourceCode.getText().length;
232
+ const newLines = "\n".repeat(rootBlockPadding);
233
+ return fixer.replaceTextRange([endOfLastNode.range[1], end], newLines);
234
+ },
235
+ });
236
+ }
237
+ }
238
+
239
+
240
+ return {
241
+ Program (node) {
242
+ const body = node.body
243
+ if (body.length === 0)
244
+ return;
245
+
246
+ checkBeginningPadding(body[0])
247
+ checkEndPadding(body[body.length - 1])
248
+ checkRootLevelBlankLines(body)
249
+
250
+ body.forEach(childNode => {
251
+ checkDocstringPadding(childNode)
252
+ });
253
+ },
254
+ BlockStatement (node) {
255
+ if (node.body.length > 1) {
256
+ checkNestedBlankLines(node, node.body)
257
+
258
+ node.body.forEach(childNode => {
259
+ checkDocstringPadding(childNode)
260
+ });
261
+ }
262
+ },
263
+ SwitchStatement (node) {
264
+ if (node.cases.length > 1) checkNestedBlankLines(node, node.cases);
265
+ },
266
+ ClassBody (node) {
267
+ if (node.body.length > 1) {
268
+ checkNestedBlankLines(node, node.body)
269
+
270
+ node.body.forEach(childNode => {
271
+ checkDocstringPadding(childNode)
272
+ });
273
+ }
274
+ },
275
+ FunctionDeclaration (node) {
276
+ checkDocstringPadding(node)
277
+ },
278
+ ClassDeclaration (node) {
279
+ checkDocstringPadding(node)
280
+ },
281
+ MethodDefinition (node) {
282
+ checkDocstringPadding(node)
283
+ },
284
+ ArrowFunctionExpression (node) {
285
+ if (node.parent.type === 'VariableDeclarator') checkDocstringPadding(node);
286
+ },
287
+ }
288
+ },
289
+ }