@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.
- package/.github/workflows/ci.yml +23 -0
- package/.github/workflows/publish.yml +45 -0
- package/AGENTS.md +29 -0
- package/bun.lock +60 -102
- package/eslint.config.mjs +1 -0
- package/index.mjs +7 -21
- package/package.json +11 -19
- package/plugins/no-inline-types/index.mjs +11 -0
- package/plugins/no-inline-types/rules/no-inline-multiline-types.mjs +181 -0
- package/plugins/omit/index.mjs +8 -0
- package/plugins/omit/rules/omit-unnecessary-parens-brackets.mjs +329 -0
- package/plugins/omit/utils.mjs +91 -0
- package/plugins/react-strict/index.mjs +19 -0
- package/plugins/react-strict/rules/jsx-prop-layout.mjs +100 -0
- package/plugins/react-strict/rules/no-complex-jsx-map.mjs +66 -0
- package/plugins/react-strict/rules/no-jsx-value-calculations.mjs +99 -0
- package/plugins/react-strict/rules/no-nested-divs.mjs +59 -0
- package/plugins/react-strict/rules/no-style-prop.mjs +43 -0
- package/plugins/react-strict/rules/prefer-no-use-effect.mjs +26 -0
- package/plugins/whitespaced/index.mjs +15 -0
- package/plugins/whitespaced/rules/aligned-assignments.mjs +385 -0
- package/plugins/whitespaced/rules/block-padding.mjs +289 -0
- package/plugins/whitespaced/rules/class-property-grouping.mjs +370 -0
- package/plugins/whitespaced/rules/consistent-line-spacing.mjs +266 -0
- package/plugins/whitespaced/rules/multiline-format.mjs +533 -0
- package/rules.mjs +101 -95
- package/test/fixtures/basic-javascript.js +5 -4
- package/test/fixtures/complex-patterns.ts +9 -7
- package/test/fixtures/edge-cases.js +12 -7
- package/test/fixtures/jsx-formatting.jsx +5 -4
- package/test/fixtures/omit-parens.invalid.ts +12 -0
- package/test/fixtures/omit-parens.valid.ts +13 -0
- package/test/fixtures/react-component.tsx +7 -6
- package/test/fixtures/react-strict.invalid.tsx +31 -0
- package/test/fixtures/react-strict.valid.tsx +76 -0
- package/test/fixtures/whitespaced-docstring.invalid.ts +10 -0
- package/test/fixtures/whitespaced-docstring.valid.ts +16 -0
- package/test/fixtures/whitespaced-members.invalid.ts +22 -0
- package/test/fixtures/whitespaced-members.valid.ts +13 -0
- package/test/fixtures/whitespaced-multiline.invalid.ts +8 -0
- package/test/fixtures/whitespaced-multiline.valid.ts +15 -0
- package/test/fixtures/whitespaced-types.valid.ts +5 -0
- package/test/fixtures/whitespaced.valid.ts +45 -0
- package/test/format-cases.mjs +13 -14
- 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
|
+
}
|