eslint-plugin-absolute 0.2.7 → 0.2.8

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.
@@ -1,581 +0,0 @@
1
- import { TSESLint, TSESTree } from "@typescript-eslint/utils";
2
-
3
- /**
4
- * @fileoverview Enforce that top-level export declarations are sorted.
5
- *
6
- * This rule supports the following options:
7
- * - order: "asc" or "desc" (default: "asc")
8
- * - caseSensitive: boolean (default: false)
9
- * - natural: boolean (default: false)
10
- * - minKeys: integer, minimum number of exports in a contiguous block to check (default: 2)
11
- * - variablesBeforeFunctions: boolean (default: false)
12
- */
13
-
14
- type SortExportsOptions = {
15
- order?: "asc" | "desc";
16
- caseSensitive?: boolean;
17
- natural?: boolean;
18
- minKeys?: number;
19
- variablesBeforeFunctions?: boolean;
20
- };
21
-
22
- type Options = [SortExportsOptions?];
23
-
24
- type MessageIds = "alphabetical" | "variablesBeforeFunctions";
25
-
26
- type ExportItem = {
27
- name: string;
28
- node: TSESTree.ExportNamedDeclaration;
29
- isFunction: boolean;
30
- text: string;
31
- };
32
-
33
- const SORT_BEFORE = Number.parseInt("-1", 10);
34
-
35
- const hasStringTypeProperty = (value: object) => {
36
- const maybeType = Reflect.get(value, "type");
37
- return typeof maybeType === "string";
38
- };
39
-
40
- const isNodeLike = (value: unknown): value is TSESTree.Node =>
41
- value !== null &&
42
- value !== undefined &&
43
- typeof value === "object" &&
44
- "type" in value &&
45
- hasStringTypeProperty(value);
46
-
47
- const shouldSkipNodeEntry = (key: string, value: unknown) =>
48
- key === "parent" || value === null || value === undefined;
49
-
50
- const visitNodeArray = (
51
- values: unknown[],
52
- visit: (node: TSESTree.Node | null | undefined) => void
53
- ) => values.filter(isNodeLike).forEach(visit);
54
-
55
- const visitNodeEntryValue = (
56
- value: unknown,
57
- visit: (node: TSESTree.Node | null | undefined) => void
58
- ) => {
59
- if (Array.isArray(value)) {
60
- visitNodeArray(value, visit);
61
- return;
62
- }
63
-
64
- if (isNodeLike(value)) {
65
- visit(value);
66
- }
67
- };
68
-
69
- const visitNodeEntries = (
70
- current: TSESTree.Node,
71
- visit: (node: TSESTree.Node | null | undefined) => void
72
- ) =>
73
- Object.entries(current)
74
- .filter(([key, value]) => !shouldSkipNodeEntry(key, value))
75
- .forEach(([, value]) => {
76
- visitNodeEntryValue(value, visit);
77
- });
78
-
79
- const getVariableDeclaratorName = (
80
- declaration: TSESTree.VariableDeclaration
81
- ) => {
82
- if (declaration.declarations.length !== 1) {
83
- return null;
84
- }
85
- const [firstDeclarator] = declaration.declarations;
86
- if (firstDeclarator && firstDeclarator.id.type === "Identifier") {
87
- return firstDeclarator.id.name;
88
- }
89
- return null;
90
- };
91
-
92
- const getDeclarationName = (
93
- declaration: TSESTree.ExportNamedDeclaration["declaration"]
94
- ) => {
95
- if (!declaration) {
96
- return null;
97
- }
98
-
99
- if (declaration.type === "VariableDeclaration") {
100
- return getVariableDeclaratorName(declaration);
101
- }
102
-
103
- if (
104
- (declaration.type === "FunctionDeclaration" ||
105
- declaration.type === "ClassDeclaration") &&
106
- declaration.id &&
107
- declaration.id.type === "Identifier"
108
- ) {
109
- return declaration.id.name;
110
- }
111
-
112
- return null;
113
- };
114
-
115
- const getSpecifierName = (node: TSESTree.ExportNamedDeclaration) => {
116
- if (node.specifiers.length !== 1) {
117
- return null;
118
- }
119
- const [spec] = node.specifiers;
120
- if (!spec) {
121
- return null;
122
- }
123
- if (spec.exported.type === "Identifier") {
124
- return spec.exported.name;
125
- }
126
- if (
127
- spec.exported.type === "Literal" &&
128
- typeof spec.exported.value === "string"
129
- ) {
130
- return spec.exported.value;
131
- }
132
- return null;
133
- };
134
-
135
- const getExportName = (node: TSESTree.ExportNamedDeclaration) =>
136
- getDeclarationName(node.declaration) ?? getSpecifierName(node);
137
-
138
- const isFixableExport = (exportNode: TSESTree.ExportNamedDeclaration) => {
139
- const { declaration } = exportNode;
140
-
141
- if (!declaration) {
142
- return exportNode.specifiers.length === 1;
143
- }
144
-
145
- if (
146
- declaration.type === "VariableDeclaration" &&
147
- declaration.declarations.length === 1
148
- ) {
149
- const [firstDecl] = declaration.declarations;
150
- return firstDecl !== undefined && firstDecl.id.type === "Identifier";
151
- }
152
-
153
- return (
154
- (declaration.type === "FunctionDeclaration" ||
155
- declaration.type === "ClassDeclaration") &&
156
- declaration.id !== null &&
157
- declaration.id.type === "Identifier"
158
- );
159
- };
160
-
161
- const visitImmediateReferences = (
162
- node: TSESTree.Node | null | undefined,
163
- onReference: (name: string) => void
164
- ) => {
165
- if (!node) {
166
- return;
167
- }
168
-
169
- const visit = (current: TSESTree.Node | null | undefined) => {
170
- if (!current) {
171
- return;
172
- }
173
-
174
- switch (current.type) {
175
- case "Identifier":
176
- onReference(current.name);
177
- return;
178
- case "FunctionDeclaration":
179
- case "FunctionExpression":
180
- case "ArrowFunctionExpression":
181
- return;
182
- case "MemberExpression":
183
- visit(current.object);
184
- if (current.computed) {
185
- visit(current.property);
186
- }
187
- return;
188
- case "Property":
189
- if (current.computed) {
190
- visit(current.key);
191
- }
192
- visit(current.value);
193
- return;
194
- case "PropertyDefinition":
195
- if (current.computed) {
196
- visit(current.key);
197
- }
198
- if (current.static) {
199
- visit(current.value);
200
- }
201
- return;
202
- case "MethodDefinition":
203
- if (current.computed) {
204
- visit(current.key);
205
- }
206
- return;
207
- case "StaticBlock":
208
- for (const statement of current.body) {
209
- visit(statement);
210
- }
211
- return;
212
- }
213
-
214
- visitNodeEntries(current, visit);
215
- };
216
-
217
- visit(node);
218
- };
219
-
220
- const getImmediateDependencyNames = (node: TSESTree.ExportNamedDeclaration) => {
221
- const names = new Set<string>();
222
- const { declaration } = node;
223
- const addName = names.add.bind(names);
224
- const addDeclaratorDependencies = (
225
- declarator: TSESTree.VariableDeclarator
226
- ) => visitImmediateReferences(declarator.init, addName);
227
- const addClassElementDependencies = (
228
- element: TSESTree.ClassElement | TSESTree.StaticBlock
229
- ) => visitImmediateReferences(element, addName);
230
-
231
- if (!declaration) {
232
- return names;
233
- }
234
-
235
- if (declaration.type === "VariableDeclaration") {
236
- declaration.declarations.forEach(addDeclaratorDependencies);
237
- return names;
238
- }
239
-
240
- if (declaration.type === "ClassDeclaration") {
241
- visitImmediateReferences(declaration.superClass, addName);
242
- declaration.body.body.forEach(addClassElementDependencies);
243
- }
244
-
245
- return names;
246
- };
247
-
248
- export const sortExports: TSESLint.RuleModule<MessageIds, Options> = {
249
- create(context) {
250
- const { sourceCode } = context;
251
- const [option] = context.options;
252
-
253
- const order: "asc" | "desc" =
254
- option && option.order ? option.order : "asc";
255
-
256
- const caseSensitive =
257
- option && typeof option.caseSensitive === "boolean"
258
- ? option.caseSensitive
259
- : false;
260
-
261
- const natural =
262
- option && typeof option.natural === "boolean"
263
- ? option.natural
264
- : false;
265
-
266
- const minKeys =
267
- option && typeof option.minKeys === "number" ? option.minKeys : 2;
268
-
269
- const variablesBeforeFunctions =
270
- option && typeof option.variablesBeforeFunctions === "boolean"
271
- ? option.variablesBeforeFunctions
272
- : false;
273
-
274
- const generateExportText = (node: TSESTree.ExportNamedDeclaration) =>
275
- sourceCode
276
- .getText(node)
277
- .trim()
278
- .replace(/\s*;?\s*$/, ";");
279
-
280
- const compareStrings = (strLeft: string, strRight: string) => {
281
- let left = strLeft;
282
- let right = strRight;
283
-
284
- if (!caseSensitive) {
285
- left = left.toLowerCase();
286
- right = right.toLowerCase();
287
- }
288
-
289
- const cmp = natural
290
- ? left.localeCompare(right, undefined, { numeric: true })
291
- : left.localeCompare(right);
292
-
293
- return order === "asc" ? cmp : -cmp;
294
- };
295
-
296
- const isFunctionExport = (node: TSESTree.ExportNamedDeclaration) => {
297
- const { declaration } = node;
298
-
299
- if (!declaration) {
300
- return false;
301
- }
302
-
303
- if (declaration.type === "FunctionDeclaration") {
304
- return true;
305
- }
306
-
307
- if (declaration.type !== "VariableDeclaration") {
308
- return false;
309
- }
310
-
311
- if (declaration.declarations.length !== 1) {
312
- return false;
313
- }
314
-
315
- const [firstDeclarator] = declaration.declarations;
316
- if (!firstDeclarator) {
317
- return false;
318
- }
319
- const { init } = firstDeclarator;
320
- if (!init) {
321
- return false;
322
- }
323
- return (
324
- init.type === "FunctionExpression" ||
325
- init.type === "ArrowFunctionExpression"
326
- );
327
- };
328
-
329
- const sortComparator = (left: ExportItem, right: ExportItem) => {
330
- const kindA = left.node.exportKind ?? "value";
331
- const kindB = right.node.exportKind ?? "value";
332
-
333
- if (kindA !== kindB) {
334
- return kindA === "type" ? SORT_BEFORE : 1;
335
- }
336
-
337
- if (
338
- variablesBeforeFunctions &&
339
- left.isFunction !== right.isFunction
340
- ) {
341
- return left.isFunction ? 1 : SORT_BEFORE;
342
- }
343
-
344
- return compareStrings(left.name, right.name);
345
- };
346
-
347
- const buildItems = (block: TSESTree.ExportNamedDeclaration[]) =>
348
- block
349
- .map((node) => {
350
- const name = getExportName(node);
351
- if (!name) {
352
- return null;
353
- }
354
- const item: ExportItem = {
355
- isFunction: isFunctionExport(node),
356
- name,
357
- node,
358
- text: sourceCode.getText(node)
359
- };
360
- return item;
361
- })
362
- .filter((item): item is ExportItem => item !== null);
363
-
364
- const findFirstUnsorted = (items: ExportItem[]) => {
365
- let messageId: MessageIds = "alphabetical";
366
-
367
- const unsorted = items.some((current, idx) => {
368
- if (idx === 0) {
369
- return false;
370
- }
371
- const prev = items[idx - 1];
372
- if (!prev) {
373
- return false;
374
- }
375
- if (sortComparator(prev, current) <= 0) {
376
- return false;
377
- }
378
- if (
379
- variablesBeforeFunctions &&
380
- prev.isFunction &&
381
- !current.isFunction
382
- ) {
383
- messageId = "variablesBeforeFunctions";
384
- }
385
- return true;
386
- });
387
-
388
- return unsorted ? messageId : null;
389
- };
390
-
391
- const hasForwardDependenciesInOrder = (items: ExportItem[]) => {
392
- const exportNames = items.map((item) => item.name);
393
- return items.some((item, idx) => {
394
- const laterNames = new Set(exportNames.slice(idx + 1));
395
- if (laterNames.size === 0) {
396
- return false;
397
- }
398
-
399
- const dependencies = getImmediateDependencyNames(item.node);
400
- for (const dependency of dependencies) {
401
- if (laterNames.has(dependency)) {
402
- return true;
403
- }
404
- }
405
- return false;
406
- });
407
- };
408
-
409
- const wouldCreateForwardDependencies = (
410
- items: ExportItem[],
411
- sortedItems: ExportItem[]
412
- ) => {
413
- const sortedIndices = new Map(
414
- sortedItems.map((item, idx) => [item.name, idx])
415
- );
416
- const exportNames = new Set(items.map((item) => item.name));
417
-
418
- return items.some((item) => {
419
- const itemIndex = sortedIndices.get(item.name);
420
- if (itemIndex === undefined) {
421
- return false;
422
- }
423
-
424
- const dependencies = getImmediateDependencyNames(item.node);
425
- for (const dependency of dependencies) {
426
- const dependencyIndex = exportNames.has(dependency)
427
- ? sortedIndices.get(dependency)
428
- : undefined;
429
- if (
430
- dependencyIndex !== undefined &&
431
- itemIndex < dependencyIndex
432
- )
433
- return true;
434
- }
435
-
436
- return false;
437
- });
438
- };
439
-
440
- const processExportBlock = (
441
- block: TSESTree.ExportNamedDeclaration[]
442
- ) => {
443
- if (block.length < minKeys) {
444
- return;
445
- }
446
-
447
- const items = buildItems(block);
448
-
449
- if (items.length < minKeys) {
450
- return;
451
- }
452
-
453
- const messageId = findFirstUnsorted(items);
454
- if (!messageId) {
455
- return;
456
- }
457
-
458
- if (hasForwardDependenciesInOrder(items)) {
459
- return;
460
- }
461
-
462
- const sortedItems = items.slice().sort(sortComparator);
463
-
464
- if (wouldCreateForwardDependencies(items, sortedItems)) {
465
- return;
466
- }
467
-
468
- const expectedOrder = sortedItems
469
- .map((item) => item.name)
470
- .join(", ");
471
-
472
- const [firstNode] = block;
473
- const lastNode = block[block.length - 1];
474
-
475
- if (!firstNode || !lastNode) {
476
- return;
477
- }
478
-
479
- context.report({
480
- data: {
481
- expectedOrder
482
- },
483
- fix(fixer) {
484
- const fixableNodes = block.filter(isFixableExport);
485
-
486
- if (fixableNodes.length < minKeys) {
487
- return null;
488
- }
489
-
490
- const sortedText = sortedItems
491
- .map((item) => generateExportText(item.node))
492
- .join("\n");
493
-
494
- const [rangeStart] = firstNode.range;
495
- const [, rangeEnd] = lastNode.range;
496
-
497
- const fullText = sourceCode.getText();
498
- const originalText = fullText.slice(rangeStart, rangeEnd);
499
-
500
- if (originalText === sortedText) {
501
- return null;
502
- }
503
-
504
- return fixer.replaceTextRange(
505
- [rangeStart, rangeEnd],
506
- sortedText
507
- );
508
- },
509
- messageId,
510
- node: firstNode
511
- });
512
- };
513
-
514
- return {
515
- "Program:exit"(node: TSESTree.Program) {
516
- const { body } = node;
517
- const block: TSESTree.ExportNamedDeclaration[] = [];
518
-
519
- body.forEach((stmt) => {
520
- if (
521
- stmt.type === "ExportNamedDeclaration" &&
522
- !stmt.source &&
523
- getExportName(stmt) !== null
524
- ) {
525
- block.push(stmt);
526
- return;
527
- }
528
-
529
- if (block.length > 0) {
530
- processExportBlock(block);
531
- block.length = 0;
532
- }
533
- });
534
-
535
- if (block.length > 0) {
536
- processExportBlock(block);
537
- }
538
- }
539
- };
540
- },
541
- defaultOptions: [{}],
542
- meta: {
543
- docs: {
544
- description:
545
- "Enforce that top-level export declarations are sorted by exported name and, optionally, that variable exports come before function exports"
546
- },
547
- fixable: "code",
548
- messages: {
549
- alphabetical:
550
- "Export declarations are not sorted alphabetically. Expected order: {{expectedOrder}}.",
551
- variablesBeforeFunctions:
552
- "Non-function exports should come before function exports."
553
- },
554
- schema: [
555
- {
556
- additionalProperties: false,
557
- properties: {
558
- caseSensitive: {
559
- type: "boolean"
560
- },
561
- minKeys: {
562
- minimum: 2,
563
- type: "integer"
564
- },
565
- natural: {
566
- type: "boolean"
567
- },
568
- order: {
569
- enum: ["asc", "desc"],
570
- type: "string"
571
- },
572
- variablesBeforeFunctions: {
573
- type: "boolean"
574
- }
575
- },
576
- type: "object"
577
- }
578
- ],
579
- type: "suggestion"
580
- }
581
- };