eslint-plugin-absolute 0.2.0 → 0.2.2

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.
@@ -30,52 +30,225 @@ type ExportItem = {
30
30
  text: string;
31
31
  };
32
32
 
33
- export const sortExports: TSESLint.RuleModule<MessageIds, Options> = {
34
- meta: {
35
- type: "suggestion",
36
- docs: {
37
- description:
38
- "Enforce that top-level export declarations are sorted by exported name and, optionally, that variable exports come before function exports"
39
- },
40
- fixable: "code",
41
- schema: [
42
- {
43
- type: "object",
44
- properties: {
45
- order: {
46
- type: "string",
47
- enum: ["asc", "desc"]
48
- },
49
- caseSensitive: {
50
- type: "boolean"
51
- },
52
- natural: {
53
- type: "boolean"
54
- },
55
- minKeys: {
56
- type: "integer",
57
- minimum: 2
58
- },
59
- variablesBeforeFunctions: {
60
- type: "boolean"
61
- }
62
- },
63
- additionalProperties: false
64
- }
65
- ],
66
- messages: {
67
- alphabetical:
68
- "Export declarations are not sorted alphabetically. Expected order: {{expectedOrder}}.",
69
- variablesBeforeFunctions:
70
- "Non-function exports should come before function exports."
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;
71
172
  }
72
- },
73
173
 
74
- defaultOptions: [{}],
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
+ };
75
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> = {
76
249
  create(context) {
77
- const sourceCode = context.sourceCode;
78
- const option = context.options[0];
250
+ const { sourceCode } = context;
251
+ const [option] = context.options;
79
252
 
80
253
  const order: "asc" | "desc" =
81
254
  option && option.order ? option.order : "asc";
@@ -98,217 +271,170 @@ export const sortExports: TSESLint.RuleModule<MessageIds, Options> = {
98
271
  ? option.variablesBeforeFunctions
99
272
  : false;
100
273
 
101
- function generateExportText(node: TSESTree.ExportNamedDeclaration) {
102
- return sourceCode
274
+ const generateExportText = (node: TSESTree.ExportNamedDeclaration) =>
275
+ sourceCode
103
276
  .getText(node)
104
277
  .trim()
105
278
  .replace(/\s*;?\s*$/, ";");
106
- }
107
279
 
108
- function compareStrings(a: string, b: string) {
109
- let strA = a;
110
- let strB = b;
280
+ const compareStrings = (strLeft: string, strRight: string) => {
281
+ let left = strLeft;
282
+ let right = strRight;
111
283
 
112
284
  if (!caseSensitive) {
113
- strA = strA.toLowerCase();
114
- strB = strB.toLowerCase();
285
+ left = left.toLowerCase();
286
+ right = right.toLowerCase();
115
287
  }
116
288
 
117
289
  const cmp = natural
118
- ? strA.localeCompare(strB, undefined, { numeric: true })
119
- : strA.localeCompare(strB);
290
+ ? left.localeCompare(right, undefined, { numeric: true })
291
+ : left.localeCompare(right);
120
292
 
121
293
  return order === "asc" ? cmp : -cmp;
122
- }
123
-
124
- function getExportName(
125
- node: TSESTree.ExportNamedDeclaration
126
- ): string | null {
127
- const declaration = node.declaration;
128
-
129
- if (declaration) {
130
- if (declaration.type === "VariableDeclaration") {
131
- if (declaration.declarations.length === 1) {
132
- const firstDeclarator = declaration.declarations[0];
133
- if (
134
- firstDeclarator &&
135
- firstDeclarator.id.type === "Identifier"
136
- ) {
137
- return firstDeclarator.id.name;
138
- }
139
- }
140
- } else if (
141
- declaration.type === "FunctionDeclaration" ||
142
- declaration.type === "ClassDeclaration"
143
- ) {
144
- const id = declaration.id;
145
- if (id && id.type === "Identifier") {
146
- return id.name;
147
- }
148
- }
149
- } else if (node.specifiers.length === 1) {
150
- const spec = node.specifiers[0];
151
- if (!spec) {
152
- return null;
153
- }
154
- if (spec.exported.type === "Identifier") {
155
- return spec.exported.name;
156
- }
157
- if (
158
- spec.exported.type === "Literal" &&
159
- typeof spec.exported.value === "string"
160
- ) {
161
- return spec.exported.value;
162
- }
163
- }
164
-
165
- return null;
166
- }
294
+ };
167
295
 
168
- function isFunctionExport(node: TSESTree.ExportNamedDeclaration) {
169
- const declaration = node.declaration;
296
+ const isFunctionExport = (node: TSESTree.ExportNamedDeclaration) => {
297
+ const { declaration } = node;
170
298
 
171
299
  if (!declaration) {
172
300
  return false;
173
301
  }
174
302
 
175
- if (declaration.type === "VariableDeclaration") {
176
- if (declaration.declarations.length === 1) {
177
- const firstDeclarator = declaration.declarations[0];
178
- if (!firstDeclarator) {
179
- return false;
180
- }
181
- const init = firstDeclarator.init;
182
- if (!init) {
183
- return false;
184
- }
185
- return (
186
- init.type === "FunctionExpression" ||
187
- init.type === "ArrowFunctionExpression"
188
- );
189
- }
190
- return false;
191
- }
192
-
193
303
  if (declaration.type === "FunctionDeclaration") {
194
304
  return true;
195
305
  }
196
306
 
197
- return false;
198
- }
199
-
200
- function sortComparator(a: ExportItem, b: ExportItem) {
201
- const kindA = a.node.exportKind ?? "value";
202
- const kindB = b.node.exportKind ?? "value";
203
-
204
- if (kindA !== kindB) {
205
- return kindA === "type" ? -1 : 1;
307
+ if (declaration.type !== "VariableDeclaration") {
308
+ return false;
206
309
  }
207
310
 
208
- if (variablesBeforeFunctions) {
209
- if (a.isFunction !== b.isFunction) {
210
- return a.isFunction ? 1 : -1;
211
- }
311
+ if (declaration.declarations.length !== 1) {
312
+ return false;
212
313
  }
213
314
 
214
- return compareStrings(a.name, b.name);
215
- }
216
-
217
- /**
218
- * Very lightweight dependency check: look at the text of the node and see
219
- * if it references any of the later export names. This avoids reordering
220
- * when there might be a forward dependency.
221
- */
222
- function hasForwardDependency(
223
- node: TSESTree.Node,
224
- laterNames: Set<string>
225
- ) {
226
- const text = sourceCode.getText(node);
227
- for (const name of laterNames) {
228
- if (text.includes(name)) {
229
- return true;
230
- }
315
+ const [firstDeclarator] = declaration.declarations;
316
+ if (!firstDeclarator) {
317
+ return false;
231
318
  }
232
- return false;
233
- }
234
-
235
- function processExportBlock(block: TSESTree.ExportNamedDeclaration[]) {
236
- if (block.length < minKeys) {
237
- return;
319
+ const { init } = firstDeclarator;
320
+ if (!init) {
321
+ return false;
238
322
  }
323
+ return (
324
+ init.type === "FunctionExpression" ||
325
+ init.type === "ArrowFunctionExpression"
326
+ );
327
+ };
239
328
 
240
- const items: ExportItem[] = [];
329
+ const sortComparator = (left: ExportItem, right: ExportItem) => {
330
+ const kindA = left.node.exportKind ?? "value";
331
+ const kindB = right.node.exportKind ?? "value";
241
332
 
242
- for (const node of block) {
243
- const name = getExportName(node);
244
- if (!name) {
245
- continue;
246
- }
247
-
248
- items.push({
249
- name,
250
- node,
251
- isFunction: isFunctionExport(node),
252
- text: sourceCode.getText(node)
253
- });
333
+ if (kindA !== kindB) {
334
+ return kindA === "type" ? SORT_BEFORE : 1;
254
335
  }
255
336
 
256
- if (items.length < minKeys) {
257
- return;
337
+ if (
338
+ variablesBeforeFunctions &&
339
+ left.isFunction !== right.isFunction
340
+ ) {
341
+ return left.isFunction ? 1 : SORT_BEFORE;
258
342
  }
259
343
 
260
- const sortedItems = items.slice().sort(sortComparator);
344
+ return compareStrings(left.name, right.name);
345
+ };
261
346
 
262
- let reportNeeded = false;
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[]) => {
263
365
  let messageId: MessageIds = "alphabetical";
264
366
 
265
- for (let i = 1; i < items.length; i++) {
266
- const prev = items[i - 1];
267
- const current = items[i];
268
-
269
- if (!prev || !current) {
270
- continue;
367
+ const unsorted = items.some((current, idx) => {
368
+ if (idx === 0) {
369
+ return false;
271
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
+ });
272
387
 
273
- if (sortComparator(prev, current) > 0) {
274
- reportNeeded = true;
388
+ return unsorted ? messageId : null;
389
+ };
275
390
 
276
- if (
277
- variablesBeforeFunctions &&
278
- prev.isFunction &&
279
- !current.isFunction
280
- ) {
281
- messageId = "variablesBeforeFunctions";
391
+ const checkForwardDependencies = (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;
282
403
  }
283
- break;
284
404
  }
285
- }
405
+ return false;
406
+ });
407
+ };
286
408
 
287
- if (!reportNeeded) {
409
+ const processExportBlock = (
410
+ block: TSESTree.ExportNamedDeclaration[]
411
+ ) => {
412
+ if (block.length < minKeys) {
288
413
  return;
289
414
  }
290
415
 
291
- const exportNames = items.map((item) => item.name);
416
+ const items = buildItems(block);
292
417
 
293
- for (let i = 0; i < items.length; i++) {
294
- const item = items[i];
295
- if (!item) {
296
- continue;
297
- }
298
- const laterNames = new Set(exportNames.slice(i + 1));
299
- const nodeToCheck: TSESTree.Node =
300
- item.node.declaration ?? item.node;
418
+ if (items.length < minKeys) {
419
+ return;
420
+ }
301
421
 
302
- if (hasForwardDependency(nodeToCheck, laterNames)) {
303
- return;
304
- }
422
+ const messageId = findFirstUnsorted(items);
423
+ if (!messageId) {
424
+ return;
425
+ }
426
+
427
+ if (checkForwardDependencies(items)) {
428
+ return;
305
429
  }
306
430
 
431
+ const sortedItems = items.slice().sort(sortComparator);
432
+
307
433
  const expectedOrder = sortedItems
308
434
  .map((item) => item.name)
309
435
  .join(", ");
310
436
 
311
- const firstNode = block[0];
437
+ const [firstNode] = block;
312
438
  const lastNode = block[block.length - 1];
313
439
 
314
440
  if (!firstNode || !lastNode) {
@@ -316,49 +442,11 @@ export const sortExports: TSESLint.RuleModule<MessageIds, Options> = {
316
442
  }
317
443
 
318
444
  context.report({
319
- node: firstNode,
320
- messageId,
321
445
  data: {
322
446
  expectedOrder
323
447
  },
324
448
  fix(fixer) {
325
- const fixableNodes: TSESTree.ExportNamedDeclaration[] = [];
326
-
327
- for (const n of block) {
328
- const declaration = n.declaration;
329
-
330
- if (declaration) {
331
- if (
332
- declaration.type === "VariableDeclaration" &&
333
- declaration.declarations.length === 1
334
- ) {
335
- const firstDecl = declaration.declarations[0];
336
- if (
337
- firstDecl &&
338
- firstDecl.id.type === "Identifier"
339
- ) {
340
- fixableNodes.push(n);
341
- continue;
342
- }
343
- }
344
-
345
- if (
346
- (declaration.type === "FunctionDeclaration" ||
347
- declaration.type === "ClassDeclaration") &&
348
- declaration.id &&
349
- declaration.id.type === "Identifier"
350
- ) {
351
- fixableNodes.push(n);
352
- continue;
353
- }
354
-
355
- continue;
356
- }
357
-
358
- if (n.specifiers.length === 1) {
359
- fixableNodes.push(n);
360
- }
361
- }
449
+ const fixableNodes = block.filter(isFixableExport);
362
450
 
363
451
  if (fixableNodes.length < minKeys) {
364
452
  return null;
@@ -368,8 +456,8 @@ export const sortExports: TSESLint.RuleModule<MessageIds, Options> = {
368
456
  .map((item) => generateExportText(item.node))
369
457
  .join("\n");
370
458
 
371
- const rangeStart = firstNode.range[0];
372
- const rangeEnd = lastNode.range[1];
459
+ const [rangeStart] = firstNode.range;
460
+ const [, rangeEnd] = lastNode.range;
373
461
 
374
462
  const fullText = sourceCode.getText();
375
463
  const originalText = fullText.slice(rangeStart, rangeEnd);
@@ -382,39 +470,77 @@ export const sortExports: TSESLint.RuleModule<MessageIds, Options> = {
382
470
  [rangeStart, rangeEnd],
383
471
  sortedText
384
472
  );
385
- }
473
+ },
474
+ messageId,
475
+ node: firstNode
386
476
  });
387
- }
477
+ };
388
478
 
389
479
  return {
390
480
  "Program:exit"(node: TSESTree.Program) {
391
- const body = node.body;
481
+ const { body } = node;
392
482
  const block: TSESTree.ExportNamedDeclaration[] = [];
393
483
 
394
- for (let i = 0; i < body.length; i++) {
395
- const stmt = body[i];
396
- if (!stmt) {
397
- continue;
398
- }
399
-
484
+ body.forEach((stmt) => {
400
485
  if (
401
486
  stmt.type === "ExportNamedDeclaration" &&
402
487
  !stmt.source &&
403
488
  getExportName(stmt) !== null
404
489
  ) {
405
490
  block.push(stmt);
406
- } else {
407
- if (block.length > 0) {
408
- processExportBlock(block);
409
- block.length = 0;
410
- }
491
+ return;
411
492
  }
412
- }
493
+
494
+ if (block.length > 0) {
495
+ processExportBlock(block);
496
+ block.length = 0;
497
+ }
498
+ });
413
499
 
414
500
  if (block.length > 0) {
415
501
  processExportBlock(block);
416
502
  }
417
503
  }
418
504
  };
505
+ },
506
+ defaultOptions: [{}],
507
+ meta: {
508
+ docs: {
509
+ description:
510
+ "Enforce that top-level export declarations are sorted by exported name and, optionally, that variable exports come before function exports"
511
+ },
512
+ fixable: "code",
513
+ messages: {
514
+ alphabetical:
515
+ "Export declarations are not sorted alphabetically. Expected order: {{expectedOrder}}.",
516
+ variablesBeforeFunctions:
517
+ "Non-function exports should come before function exports."
518
+ },
519
+ schema: [
520
+ {
521
+ additionalProperties: false,
522
+ properties: {
523
+ caseSensitive: {
524
+ type: "boolean"
525
+ },
526
+ minKeys: {
527
+ minimum: 2,
528
+ type: "integer"
529
+ },
530
+ natural: {
531
+ type: "boolean"
532
+ },
533
+ order: {
534
+ enum: ["asc", "desc"],
535
+ type: "string"
536
+ },
537
+ variablesBeforeFunctions: {
538
+ type: "boolean"
539
+ }
540
+ },
541
+ type: "object"
542
+ }
543
+ ],
544
+ type: "suggestion"
419
545
  }
420
546
  };