@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,370 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview ESLint rule to enforce grouping of class properties
|
|
3
|
+
* @author tuomashatakka
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict'
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
10
|
+
export default {
|
|
11
|
+
meta: {
|
|
12
|
+
type: "suggestion",
|
|
13
|
+
docs: {
|
|
14
|
+
description: "Enforce grouping of class properties",
|
|
15
|
+
category: "Stylistic Issues",
|
|
16
|
+
recommended: false,
|
|
17
|
+
},
|
|
18
|
+
fixable: "code",
|
|
19
|
+
schema: [
|
|
20
|
+
{
|
|
21
|
+
type: "object",
|
|
22
|
+
properties: {
|
|
23
|
+
groups: {
|
|
24
|
+
type: "array",
|
|
25
|
+
items: {
|
|
26
|
+
type: "object",
|
|
27
|
+
properties: {
|
|
28
|
+
name: { type: "string" },
|
|
29
|
+
types: {
|
|
30
|
+
type: "array",
|
|
31
|
+
items: { type: "string" },
|
|
32
|
+
},
|
|
33
|
+
matches: {
|
|
34
|
+
type: "array",
|
|
35
|
+
items: { type: "string" },
|
|
36
|
+
},
|
|
37
|
+
order: { type: "integer", minimum: 0 },
|
|
38
|
+
},
|
|
39
|
+
required: ["name", "order"],
|
|
40
|
+
additionalProperties: false,
|
|
41
|
+
},
|
|
42
|
+
default: [
|
|
43
|
+
{
|
|
44
|
+
name: "static-properties",
|
|
45
|
+
types: ["ClassProperty"],
|
|
46
|
+
matches: ["static"],
|
|
47
|
+
order: 0,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: "static-methods",
|
|
51
|
+
types: ["MethodDefinition"],
|
|
52
|
+
matches: ["static"],
|
|
53
|
+
order: 1,
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: "instance-properties",
|
|
57
|
+
types: ["ClassProperty"],
|
|
58
|
+
matches: [],
|
|
59
|
+
order: 2,
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: "constructor",
|
|
63
|
+
types: ["MethodDefinition"],
|
|
64
|
+
matches: ["constructor"],
|
|
65
|
+
order: 3,
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: "instance-methods",
|
|
69
|
+
types: ["MethodDefinition"],
|
|
70
|
+
matches: [],
|
|
71
|
+
order: 4,
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
paddingBetweenGroups: {
|
|
76
|
+
type: "integer",
|
|
77
|
+
minimum: 0,
|
|
78
|
+
default: 1,
|
|
79
|
+
},
|
|
80
|
+
enforceAlphabeticalSorting: {
|
|
81
|
+
type: "boolean",
|
|
82
|
+
default: false,
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
additionalProperties: false,
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
messages: {
|
|
89
|
+
wrongGroupOrder:
|
|
90
|
+
"Class member '{{member}}' should be in group '{{expectedGroup}}' ({{expectedGroupOrder}}) but is in group '{{actualGroup}}' ({{actualGroupOrder}}).",
|
|
91
|
+
wrongAlphabeticalOrder:
|
|
92
|
+
"Class members in the same group should be ordered alphabetically. '{{memberA}}' should come before '{{memberB}}'.",
|
|
93
|
+
incorrectPaddingBetweenGroups:
|
|
94
|
+
"Expected {{expected}} empty {{lineText}} between class member groups, but found {{actual}}."
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
create(context) {
|
|
98
|
+
const sourceCode = context.getSourceCode();
|
|
99
|
+
const options = context.options[0] || {};
|
|
100
|
+
|
|
101
|
+
// Get configured options with defaults
|
|
102
|
+
const groups = options.groups || [
|
|
103
|
+
{
|
|
104
|
+
name: "static-properties",
|
|
105
|
+
types: ["ClassProperty"],
|
|
106
|
+
matches: ["static"],
|
|
107
|
+
order: 0,
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: "static-methods",
|
|
111
|
+
types: ["MethodDefinition"],
|
|
112
|
+
matches: ["static"],
|
|
113
|
+
order: 1,
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: "instance-properties",
|
|
117
|
+
types: ["ClassProperty"],
|
|
118
|
+
matches: [],
|
|
119
|
+
order: 2,
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: "constructor",
|
|
123
|
+
types: ["MethodDefinition"],
|
|
124
|
+
matches: ["constructor"],
|
|
125
|
+
order: 3,
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: "instance-methods",
|
|
129
|
+
types: ["MethodDefinition"],
|
|
130
|
+
matches: [],
|
|
131
|
+
order: 4,
|
|
132
|
+
},
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
const paddingBetweenGroups = options.paddingBetweenGroups !== undefined ? options.paddingBetweenGroups : 1;
|
|
136
|
+
const enforceAlphabeticalSorting = options.enforceAlphabeticalSorting !== undefined ? options.enforceAlphabeticalSorting : false;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Determine which group a class member belongs to
|
|
140
|
+
* @param {ASTNode} node The class member node
|
|
141
|
+
* @returns {Object|null} The group object or null if not matched
|
|
142
|
+
*/
|
|
143
|
+
function getMemberGroup(node) {
|
|
144
|
+
let nodeType = node.type;
|
|
145
|
+
|
|
146
|
+
// Normalize TypeScript property types to match ESLint's ClassProperty
|
|
147
|
+
if (nodeType === "PropertyDefinition" || nodeType === "TSPropertyDefinition") {
|
|
148
|
+
nodeType = "ClassProperty";
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// For each group, check if the node matches the group criteria
|
|
152
|
+
for (const group of groups) {
|
|
153
|
+
// Skip if the node type doesn't match any in the group's types
|
|
154
|
+
if (!group.types.includes(nodeType)) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// For MethodDefinition nodes
|
|
159
|
+
if (nodeType === "MethodDefinition") {
|
|
160
|
+
const isConstructor = node.kind === "constructor";
|
|
161
|
+
const isStatic = !!node.static;
|
|
162
|
+
|
|
163
|
+
// If this is a constructor and the group is for constructors
|
|
164
|
+
if (isConstructor && group.matches.includes("constructor")) {
|
|
165
|
+
return group;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// If this is a static method and the group is for static methods
|
|
169
|
+
if (isStatic && group.matches.includes("static")) {
|
|
170
|
+
return group;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// If this is not static and not a constructor and the group is for regular instance methods
|
|
174
|
+
if (!isStatic && !isConstructor && !group.matches.includes("static") && !group.matches.includes("constructor")) {
|
|
175
|
+
return group;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// For ClassProperty nodes
|
|
180
|
+
if (nodeType === "ClassProperty") {
|
|
181
|
+
const isStatic = !!node.static;
|
|
182
|
+
|
|
183
|
+
// If this is a static property and the group is for static properties
|
|
184
|
+
if (isStatic && group.matches.includes("static")) {
|
|
185
|
+
return group;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// If this is not static and the group is for regular instance properties
|
|
189
|
+
if (!isStatic && !group.matches.includes("static")) {
|
|
190
|
+
return group;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Get the member name for a node
|
|
200
|
+
* @param {ASTNode} node The class member node
|
|
201
|
+
* @returns {string} The member name
|
|
202
|
+
*/
|
|
203
|
+
function getMemberName(node) {
|
|
204
|
+
if (node.type === "MethodDefinition" || node.type === "ClassProperty" ||
|
|
205
|
+
node.type === "PropertyDefinition" || node.type === "TSPropertyDefinition") {
|
|
206
|
+
if (node.key.type === "Identifier") {
|
|
207
|
+
return node.key.name;
|
|
208
|
+
} else if (node.key.type === "Literal") {
|
|
209
|
+
return String(node.key.value);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return "";
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Check if members are in correct group order
|
|
217
|
+
* @param {Array<Object>} members Array of { node, group } objects
|
|
218
|
+
*/
|
|
219
|
+
function checkGroupOrder(members) {
|
|
220
|
+
let lastGroupOrder = -1;
|
|
221
|
+
let lastGroup = null;
|
|
222
|
+
|
|
223
|
+
for (let i = 0; i < members.length; i++) {
|
|
224
|
+
const { node, group } = members[i];
|
|
225
|
+
if (!group) continue;
|
|
226
|
+
|
|
227
|
+
const currentGroupOrder = group.order;
|
|
228
|
+
const memberName = getMemberName(node);
|
|
229
|
+
|
|
230
|
+
// Check if group order is correct
|
|
231
|
+
if (currentGroupOrder < lastGroupOrder) {
|
|
232
|
+
const lastGroupInfo = lastGroup;
|
|
233
|
+
|
|
234
|
+
context.report({
|
|
235
|
+
node,
|
|
236
|
+
messageId: "wrongGroupOrder",
|
|
237
|
+
data: {
|
|
238
|
+
member: memberName,
|
|
239
|
+
expectedGroup: lastGroupInfo.name,
|
|
240
|
+
expectedGroupOrder: lastGroupInfo.order,
|
|
241
|
+
actualGroup: group.name,
|
|
242
|
+
actualGroupOrder: group.order,
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
lastGroupOrder = currentGroupOrder;
|
|
248
|
+
lastGroup = group;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Check if members within the same group are sorted alphabetically
|
|
254
|
+
* @param {Array<Object>} members Array of { node, group } objects
|
|
255
|
+
*/
|
|
256
|
+
function checkAlphabeticalOrder(members) {
|
|
257
|
+
if (!enforceAlphabeticalSorting) return;
|
|
258
|
+
|
|
259
|
+
// Group members by their group
|
|
260
|
+
const groupedMembers = {};
|
|
261
|
+
|
|
262
|
+
for (const member of members) {
|
|
263
|
+
if (!member.group) continue;
|
|
264
|
+
|
|
265
|
+
const groupName = member.group.name;
|
|
266
|
+
if (!groupedMembers[groupName]) {
|
|
267
|
+
groupedMembers[groupName] = [];
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
groupedMembers[groupName].push(member);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Check each group for alphabetical ordering
|
|
274
|
+
for (const groupName in groupedMembers) {
|
|
275
|
+
const groupMembers = groupedMembers[groupName];
|
|
276
|
+
|
|
277
|
+
for (let i = 1; i < groupMembers.length; i++) {
|
|
278
|
+
const prevNode = groupMembers[i - 1].node;
|
|
279
|
+
const currentNode = groupMembers[i].node;
|
|
280
|
+
|
|
281
|
+
const prevName = getMemberName(prevNode);
|
|
282
|
+
const currentName = getMemberName(currentNode);
|
|
283
|
+
|
|
284
|
+
if (prevName && currentName && prevName.localeCompare(currentName) > 0) {
|
|
285
|
+
context.report({
|
|
286
|
+
node: currentNode,
|
|
287
|
+
messageId: "wrongAlphabeticalOrder",
|
|
288
|
+
data: {
|
|
289
|
+
memberA: currentName,
|
|
290
|
+
memberB: prevName,
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Check padding between different groups
|
|
300
|
+
* @param {Array<Object>} members Array of { node, group } objects
|
|
301
|
+
*/
|
|
302
|
+
function checkGroupPadding(members) {
|
|
303
|
+
if (paddingBetweenGroups <= 0 || members.length < 2) return;
|
|
304
|
+
|
|
305
|
+
let currentGroup = null;
|
|
306
|
+
|
|
307
|
+
for (let i = 1; i < members.length; i++) {
|
|
308
|
+
const prevMember = members[i - 1];
|
|
309
|
+
const currentMember = members[i];
|
|
310
|
+
|
|
311
|
+
// Skip if either member doesn't have a valid group
|
|
312
|
+
if (!prevMember.group || !currentMember.group) continue;
|
|
313
|
+
|
|
314
|
+
// If the groups are different, check padding
|
|
315
|
+
if (prevMember.group.name !== currentMember.group.name) {
|
|
316
|
+
const prevNode = prevMember.node;
|
|
317
|
+
const currentNode = currentMember.node;
|
|
318
|
+
|
|
319
|
+
const prevNodeEnd = prevNode.loc.end.line;
|
|
320
|
+
const currentNodeStart = currentNode.loc.start.line;
|
|
321
|
+
const blankLines = currentNodeStart - prevNodeEnd - 1;
|
|
322
|
+
|
|
323
|
+
if (blankLines !== paddingBetweenGroups) {
|
|
324
|
+
context.report({
|
|
325
|
+
node: currentNode,
|
|
326
|
+
messageId: "incorrectPaddingBetweenGroups",
|
|
327
|
+
data: {
|
|
328
|
+
expected: paddingBetweenGroups,
|
|
329
|
+
actual: blankLines,
|
|
330
|
+
lineText: paddingBetweenGroups === 1 ? "line" : "lines",
|
|
331
|
+
},
|
|
332
|
+
fix(fixer) {
|
|
333
|
+
const endOfPrevNode = sourceCode.getLastToken(prevNode);
|
|
334
|
+
const startOfCurrentNode = sourceCode.getFirstToken(currentNode);
|
|
335
|
+
const range = [
|
|
336
|
+
endOfPrevNode.range[1],
|
|
337
|
+
startOfCurrentNode.range[0],
|
|
338
|
+
];
|
|
339
|
+
|
|
340
|
+
// Create the desired padding
|
|
341
|
+
const newLines = "\n".repeat(paddingBetweenGroups + 1); // +1 for end of current line
|
|
342
|
+
|
|
343
|
+
return fixer.replaceTextRange(range, newLines);
|
|
344
|
+
},
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
ClassBody(node) {
|
|
353
|
+
if (!node.body || node.body.length <= 1) {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Map each node to its group
|
|
358
|
+
const members = node.body.map(memberNode => ({
|
|
359
|
+
node: memberNode,
|
|
360
|
+
group: getMemberGroup(memberNode),
|
|
361
|
+
}));
|
|
362
|
+
|
|
363
|
+
// Run checks
|
|
364
|
+
checkGroupOrder(members);
|
|
365
|
+
checkAlphabeticalOrder(members);
|
|
366
|
+
checkGroupPadding(members);
|
|
367
|
+
},
|
|
368
|
+
};
|
|
369
|
+
},
|
|
370
|
+
};
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview ESLint rule to enforce consistent line spacing before and after statements
|
|
3
|
+
* @author tuomashatakka
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
meta: {
|
|
8
|
+
type: 'layout',
|
|
9
|
+
docs: {
|
|
10
|
+
description: 'Enforce consistent line spacing before and after statements',
|
|
11
|
+
category: "Stylistic Issues",
|
|
12
|
+
recommended: false,
|
|
13
|
+
},
|
|
14
|
+
fixable: 'whitespace',
|
|
15
|
+
schema: [{
|
|
16
|
+
type: "object",
|
|
17
|
+
properties: {
|
|
18
|
+
beforeImports: { type: 'integer', minimum: 0, default: 1 },
|
|
19
|
+
afterImports: { type: 'integer', minimum: 0, default: 1 },
|
|
20
|
+
beforeExports: { type: 'integer', minimum: 0, default: 1 },
|
|
21
|
+
afterExports: { type: 'integer', minimum: 0, default: 1 },
|
|
22
|
+
beforeClass: { type: 'integer', minimum: 0, default: 2 },
|
|
23
|
+
afterClass: { type: 'integer', minimum: 0, default: 2 },
|
|
24
|
+
beforeFunction: { type: 'integer', minimum: 0, default: 2 },
|
|
25
|
+
afterFunction: { type: 'integer', minimum: 0, default: 2 },
|
|
26
|
+
beforeComment: { type: 'integer', minimum: 0, default: 1 },
|
|
27
|
+
ignoreTopLevelCode: { type: 'boolean', default: false },
|
|
28
|
+
skipImportGroups: { type: 'boolean', default: true },
|
|
29
|
+
docstringSpacing: { type: 'integer', minimum: 0, default: 0 },
|
|
30
|
+
},
|
|
31
|
+
additionalProperties: false,
|
|
32
|
+
},],
|
|
33
|
+
messages: {
|
|
34
|
+
missingLinesBefore: "Expected {{expected}} empty {{lineText}} before {{nodeType}}, but found {{actual}}.",
|
|
35
|
+
missingLinesAfter: "Expected {{expected}} empty {{lineText}} after {{nodeType}}, but found {{actual}}.",
|
|
36
|
+
incorrectDocstringSpacing: "Expected {{expected}} empty {{lineText}} between docstring and {{nodeType}}, but found {{actual}}.",
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
create (context) {
|
|
40
|
+
const sourceCode = context.sourceCode || context.getSourceCode()
|
|
41
|
+
const options = context.options[0] || {}
|
|
42
|
+
|
|
43
|
+
const beforeImports = options.beforeImports !== undefined ? options.beforeImports : 1
|
|
44
|
+
const afterImports = options.afterImports !== undefined ? options.afterImports : 1
|
|
45
|
+
const beforeExports = options.beforeExports !== undefined ? options.beforeExports : 1
|
|
46
|
+
const afterExports = options.afterExports !== undefined ? options.afterExports : 1
|
|
47
|
+
const beforeClass = options.beforeClass !== undefined ? options.beforeClass : 2
|
|
48
|
+
const afterClass = options.afterClass !== undefined ? options.afterClass : 2
|
|
49
|
+
const beforeFunction = options.beforeFunction !== undefined ? options.beforeFunction : 2
|
|
50
|
+
const afterFunction = options.afterFunction !== undefined ? options.afterFunction : 2
|
|
51
|
+
const beforeComment = options.beforeComment !== undefined ? options.beforeComment : 1
|
|
52
|
+
const ignoreTopLevelCode = options.ignoreTopLevelCode !== undefined ? options.ignoreTopLevelCode : false
|
|
53
|
+
const skipImportGroups = options.skipImportGroups !== undefined ? options.skipImportGroups : true
|
|
54
|
+
const docstringSpacing = options.docstringSpacing !== undefined ? options.docstringSpacing : 0
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
function isFirstInParent (node) {
|
|
58
|
+
const parent = node.parent
|
|
59
|
+
if (!parent || !parent.body || !parent.body.length)
|
|
60
|
+
return true
|
|
61
|
+
return parent.body[0] === node
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
function isLastInParent (node) {
|
|
66
|
+
const parent = node.parent
|
|
67
|
+
if (!parent || !parent.body || !parent.body.length)
|
|
68
|
+
return true
|
|
69
|
+
return parent.body[parent.body.length - 1] === node
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
function isTopLevel (node) {
|
|
74
|
+
return node.parent && node.parent.type === 'Program'
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
function isImport (node) {
|
|
79
|
+
return node.type === 'ImportDeclaration'
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Returns the first token of the docstring immediately preceding `node`
|
|
85
|
+
* (no blank lines between docstring end and node start), or null.
|
|
86
|
+
*
|
|
87
|
+
* Detects two docstring forms:
|
|
88
|
+
* • Block comment /* ... *\/ on the line immediately before node
|
|
89
|
+
* • One or more consecutive // line comments on the lines immediately before node
|
|
90
|
+
*/
|
|
91
|
+
function getDocstringBefore (node) {
|
|
92
|
+
const prev = sourceCode.getTokenBefore(node, { includeComments: true })
|
|
93
|
+
if (!prev)
|
|
94
|
+
return null
|
|
95
|
+
|
|
96
|
+
// Block comment touching the node (0 blank lines between)
|
|
97
|
+
if (prev.type === 'Block' && node.loc.start.line <= prev.loc.end.line + 1)
|
|
98
|
+
return prev
|
|
99
|
+
|
|
100
|
+
// Chain of consecutive line comments touching the node
|
|
101
|
+
if (prev.type === 'Line' && node.loc.start.line <= prev.loc.end.line + 1) {
|
|
102
|
+
let first = prev
|
|
103
|
+
for (;;) {
|
|
104
|
+
const p = sourceCode.getTokenBefore(first, { includeComments: true })
|
|
105
|
+
if (!p || p.type !== 'Line' || first.loc.start.line > p.loc.end.line + 1)
|
|
106
|
+
break
|
|
107
|
+
first = p
|
|
108
|
+
}
|
|
109
|
+
return first
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return null
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Checks blank lines before `node` (or before its attached docstring).
|
|
118
|
+
*
|
|
119
|
+
* Key behaviour: if a docstring immediately precedes `node`, we measure
|
|
120
|
+
* `requiredLines` blank lines from the previous non-comment code token to
|
|
121
|
+
* the **start of the docstring**, not to the node keyword itself.
|
|
122
|
+
* This way `beforeFunction: 2` works correctly even when the function has
|
|
123
|
+
* a leading JSDoc / line-comment block.
|
|
124
|
+
*/
|
|
125
|
+
function checkLinesBefore (node, requiredLines, nodeType) {
|
|
126
|
+
if (isFirstInParent(node) && ignoreTopLevelCode && isTopLevel(node))
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
const docstring = getDocstringBefore(node)
|
|
130
|
+
const effectiveNode = docstring ?? node // measure TO this node's start
|
|
131
|
+
|
|
132
|
+
// Previous non-comment code token (before the docstring if present)
|
|
133
|
+
const prevCode = sourceCode.getTokenBefore(effectiveNode, {
|
|
134
|
+
includeComments: false,
|
|
135
|
+
})
|
|
136
|
+
if (!prevCode)
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
// Skip consecutive imports when skipImportGroups is on
|
|
140
|
+
if (skipImportGroups && isImport(node)) {
|
|
141
|
+
const prevNode = sourceCode.getNodeByRangeIndex(prevCode.range[0])
|
|
142
|
+
if (prevNode && isImport(prevNode))
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const blankLines = effectiveNode.loc.start.line - prevCode.loc.end.line - 1
|
|
147
|
+
|
|
148
|
+
if (blankLines !== requiredLines)
|
|
149
|
+
context.report({
|
|
150
|
+
node: effectiveNode,
|
|
151
|
+
messageId: 'missingLinesBefore',
|
|
152
|
+
data: {
|
|
153
|
+
expected: requiredLines,
|
|
154
|
+
actual: blankLines,
|
|
155
|
+
nodeType,
|
|
156
|
+
lineText: requiredLines === 1 ? 'line' : 'lines',
|
|
157
|
+
},
|
|
158
|
+
fix(fixer) {
|
|
159
|
+
return fixer.replaceTextRange(
|
|
160
|
+
[ prevCode.range[1], effectiveNode.range[0] ],
|
|
161
|
+
'\n'.repeat(requiredLines + 1)
|
|
162
|
+
);
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Also enforce spacing between docstring end and node start
|
|
167
|
+
if (docstring) {
|
|
168
|
+
const blankBetween = node.loc.start.line - docstring.loc.end.line - 1
|
|
169
|
+
if (blankBetween !== docstringSpacing)
|
|
170
|
+
context.report({
|
|
171
|
+
node,
|
|
172
|
+
messageId: 'incorrectDocstringSpacing',
|
|
173
|
+
data: {
|
|
174
|
+
expected: docstringSpacing,
|
|
175
|
+
actual: blankBetween,
|
|
176
|
+
nodeType,
|
|
177
|
+
lineText: docstringSpacing === 1 ? 'line' : 'lines',
|
|
178
|
+
},
|
|
179
|
+
fix(fixer) {
|
|
180
|
+
return fixer.replaceTextRange(
|
|
181
|
+
[ docstring.range[1], node.range[0] ],
|
|
182
|
+
'\n'.repeat(docstringSpacing + 1)
|
|
183
|
+
);
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
function checkLinesAfter (node, requiredLines, nodeType) {
|
|
191
|
+
if (isLastInParent(node) && (ignoreTopLevelCode && isTopLevel(node)))
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
const nextToken = sourceCode.getTokenAfter(node, {
|
|
195
|
+
includeComments: false,
|
|
196
|
+
})
|
|
197
|
+
const nextNode = nextToken ? sourceCode.getNodeByRangeIndex(nextToken.range[0]) : null
|
|
198
|
+
|
|
199
|
+
if (skipImportGroups && isImport(node) && nextNode && isImport(nextNode))
|
|
200
|
+
return
|
|
201
|
+
|
|
202
|
+
const nextTokenWithComments = sourceCode.getTokenAfter(node, {
|
|
203
|
+
includeComments: true,
|
|
204
|
+
})
|
|
205
|
+
if (!nextTokenWithComments)
|
|
206
|
+
return
|
|
207
|
+
|
|
208
|
+
const blankLines = nextTokenWithComments.loc.start.line - node.loc.end.line - 1
|
|
209
|
+
|
|
210
|
+
if (blankLines !== requiredLines)
|
|
211
|
+
context.report({
|
|
212
|
+
node,
|
|
213
|
+
messageId: "missingLinesAfter",
|
|
214
|
+
data: {
|
|
215
|
+
expected: requiredLines,
|
|
216
|
+
actual: blankLines,
|
|
217
|
+
nodeType,
|
|
218
|
+
lineText: requiredLines === 1 ? "line" : "lines",
|
|
219
|
+
},
|
|
220
|
+
fix(fixer) {
|
|
221
|
+
const tokenAfter = sourceCode.getTokenAfter(node, { includeComments: true });
|
|
222
|
+
if (!tokenAfter) {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const range = [node.range[1], tokenAfter.range[0]];
|
|
227
|
+
const newLines = "\n".repeat(requiredLines + 1); // +1 because one newline is the end of the current line
|
|
228
|
+
|
|
229
|
+
return fixer.replaceTextRange(range, newLines);
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
ImportDeclaration (node) {
|
|
237
|
+
checkLinesBefore(node, beforeImports, 'import declaration')
|
|
238
|
+
checkLinesAfter(node, afterImports, 'import declaration')
|
|
239
|
+
},
|
|
240
|
+
ExportNamedDeclaration (node) {
|
|
241
|
+
checkLinesBefore(node, beforeExports, 'export declaration')
|
|
242
|
+
checkLinesAfter(node, afterExports, 'export declaration')
|
|
243
|
+
},
|
|
244
|
+
ExportDefaultDeclaration (node) {
|
|
245
|
+
checkLinesBefore(node, beforeExports, 'export declaration')
|
|
246
|
+
checkLinesAfter(node, afterExports, 'export declaration')
|
|
247
|
+
},
|
|
248
|
+
ExportAllDeclaration (node) {
|
|
249
|
+
checkLinesBefore(node, beforeExports, 'export declaration')
|
|
250
|
+
checkLinesAfter(node, afterExports, 'export declaration')
|
|
251
|
+
},
|
|
252
|
+
ClassDeclaration (node) {
|
|
253
|
+
checkLinesBefore(node, beforeClass, 'class declaration')
|
|
254
|
+
checkLinesAfter(node, afterClass, 'class declaration')
|
|
255
|
+
},
|
|
256
|
+
FunctionDeclaration (node) {
|
|
257
|
+
checkLinesBefore(node, beforeFunction, 'function declaration')
|
|
258
|
+
checkLinesAfter(node, afterFunction, 'function declaration')
|
|
259
|
+
},
|
|
260
|
+
BlockComment (node) {
|
|
261
|
+
if (node.loc.start.column === 0)
|
|
262
|
+
checkLinesBefore(node, beforeComment, 'block comment')
|
|
263
|
+
},
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
}
|