eslint-plugin-absolute 0.2.0 → 0.2.1

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,94 @@ 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."
71
- }
72
- },
33
+ const SORT_BEFORE: -1 = -1;
73
34
 
74
- defaultOptions: [{}],
35
+ const getVariableDeclaratorName = (
36
+ declaration: TSESTree.VariableDeclaration
37
+ ) => {
38
+ if (declaration.declarations.length !== 1) {
39
+ return null;
40
+ }
41
+ const [firstDeclarator] = declaration.declarations;
42
+ if (firstDeclarator && firstDeclarator.id.type === "Identifier") {
43
+ return firstDeclarator.id.name;
44
+ }
45
+ return null;
46
+ };
47
+
48
+ const getDeclarationName = (
49
+ declaration: TSESTree.ExportNamedDeclaration["declaration"]
50
+ ) => {
51
+ if (!declaration) {
52
+ return null;
53
+ }
54
+
55
+ if (declaration.type === "VariableDeclaration") {
56
+ return getVariableDeclaratorName(declaration);
57
+ }
58
+
59
+ if (
60
+ (declaration.type === "FunctionDeclaration" ||
61
+ declaration.type === "ClassDeclaration") &&
62
+ declaration.id &&
63
+ declaration.id.type === "Identifier"
64
+ ) {
65
+ return declaration.id.name;
66
+ }
67
+
68
+ return null;
69
+ };
70
+
71
+ const getSpecifierName = (node: TSESTree.ExportNamedDeclaration) => {
72
+ if (node.specifiers.length !== 1) {
73
+ return null;
74
+ }
75
+ const [spec] = node.specifiers;
76
+ if (!spec) {
77
+ return null;
78
+ }
79
+ if (spec.exported.type === "Identifier") {
80
+ return spec.exported.name;
81
+ }
82
+ if (
83
+ spec.exported.type === "Literal" &&
84
+ typeof spec.exported.value === "string"
85
+ ) {
86
+ return spec.exported.value;
87
+ }
88
+ return null;
89
+ };
90
+
91
+ const getExportName = (node: TSESTree.ExportNamedDeclaration) =>
92
+ getDeclarationName(node.declaration) ?? getSpecifierName(node);
93
+
94
+ const isFixableExport = (exportNode: TSESTree.ExportNamedDeclaration) => {
95
+ const { declaration } = exportNode;
96
+
97
+ if (!declaration) {
98
+ return exportNode.specifiers.length === 1;
99
+ }
100
+
101
+ if (
102
+ declaration.type === "VariableDeclaration" &&
103
+ declaration.declarations.length === 1
104
+ ) {
105
+ const [firstDecl] = declaration.declarations;
106
+ return firstDecl !== undefined && firstDecl.id.type === "Identifier";
107
+ }
108
+
109
+ return (
110
+ (declaration.type === "FunctionDeclaration" ||
111
+ declaration.type === "ClassDeclaration") &&
112
+ declaration.id !== null &&
113
+ declaration.id.type === "Identifier"
114
+ );
115
+ };
75
116
 
117
+ export const sortExports: TSESLint.RuleModule<MessageIds, Options> = {
76
118
  create(context) {
77
- const sourceCode = context.sourceCode;
78
- const option = context.options[0];
119
+ const { sourceCode } = context;
120
+ const [option] = context.options;
79
121
 
80
122
  const order: "asc" | "desc" =
81
123
  option && option.order ? option.order : "asc";
@@ -98,131 +140,87 @@ export const sortExports: TSESLint.RuleModule<MessageIds, Options> = {
98
140
  ? option.variablesBeforeFunctions
99
141
  : false;
100
142
 
101
- function generateExportText(node: TSESTree.ExportNamedDeclaration) {
102
- return sourceCode
143
+ const generateExportText = (node: TSESTree.ExportNamedDeclaration) =>
144
+ sourceCode
103
145
  .getText(node)
104
146
  .trim()
105
147
  .replace(/\s*;?\s*$/, ";");
106
- }
107
148
 
108
- function compareStrings(a: string, b: string) {
109
- let strA = a;
110
- let strB = b;
149
+ const compareStrings = (strLeft: string, strRight: string) => {
150
+ let left = strLeft;
151
+ let right = strRight;
111
152
 
112
153
  if (!caseSensitive) {
113
- strA = strA.toLowerCase();
114
- strB = strB.toLowerCase();
154
+ left = left.toLowerCase();
155
+ right = right.toLowerCase();
115
156
  }
116
157
 
117
158
  const cmp = natural
118
- ? strA.localeCompare(strB, undefined, { numeric: true })
119
- : strA.localeCompare(strB);
159
+ ? left.localeCompare(right, undefined, { numeric: true })
160
+ : left.localeCompare(right);
120
161
 
121
162
  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
- }
163
+ };
167
164
 
168
- function isFunctionExport(node: TSESTree.ExportNamedDeclaration) {
169
- const declaration = node.declaration;
165
+ const isFunctionExport = (node: TSESTree.ExportNamedDeclaration) => {
166
+ const { declaration } = node;
170
167
 
171
168
  if (!declaration) {
172
169
  return false;
173
170
  }
174
171
 
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
- }
172
+ if (declaration.type === "FunctionDeclaration") {
173
+ return true;
174
+ }
175
+
176
+ if (declaration.type !== "VariableDeclaration") {
190
177
  return false;
191
178
  }
192
179
 
193
- if (declaration.type === "FunctionDeclaration") {
194
- return true;
180
+ if (declaration.declarations.length !== 1) {
181
+ return false;
195
182
  }
196
183
 
197
- return false;
198
- }
184
+ const [firstDeclarator] = declaration.declarations;
185
+ if (!firstDeclarator) {
186
+ return false;
187
+ }
188
+ const { init } = firstDeclarator;
189
+ if (!init) {
190
+ return false;
191
+ }
192
+ return (
193
+ init.type === "FunctionExpression" ||
194
+ init.type === "ArrowFunctionExpression"
195
+ );
196
+ };
199
197
 
200
- function sortComparator(a: ExportItem, b: ExportItem) {
201
- const kindA = a.node.exportKind ?? "value";
202
- const kindB = b.node.exportKind ?? "value";
198
+ const sortComparator = (left: ExportItem, right: ExportItem) => {
199
+ const kindA = left.node.exportKind ?? "value";
200
+ const kindB = right.node.exportKind ?? "value";
203
201
 
204
202
  if (kindA !== kindB) {
205
- return kindA === "type" ? -1 : 1;
203
+ return kindA === "type" ? SORT_BEFORE : 1;
206
204
  }
207
205
 
208
- if (variablesBeforeFunctions) {
209
- if (a.isFunction !== b.isFunction) {
210
- return a.isFunction ? 1 : -1;
211
- }
206
+ if (
207
+ variablesBeforeFunctions &&
208
+ left.isFunction !== right.isFunction
209
+ ) {
210
+ return left.isFunction ? 1 : SORT_BEFORE;
212
211
  }
213
212
 
214
- return compareStrings(a.name, b.name);
215
- }
213
+ return compareStrings(left.name, right.name);
214
+ };
216
215
 
217
216
  /**
218
217
  * 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.
218
+ * if it references any of the later export names.
221
219
  */
222
- function hasForwardDependency(
220
+ const hasForwardDependency = (
223
221
  node: TSESTree.Node,
224
222
  laterNames: Set<string>
225
- ) {
223
+ ) => {
226
224
  const text = sourceCode.getText(node);
227
225
  for (const name of laterNames) {
228
226
  if (text.includes(name)) {
@@ -230,85 +228,91 @@ export const sortExports: TSESLint.RuleModule<MessageIds, Options> = {
230
228
  }
231
229
  }
232
230
  return false;
233
- }
234
-
235
- function processExportBlock(block: TSESTree.ExportNamedDeclaration[]) {
236
- if (block.length < minKeys) {
237
- return;
238
- }
231
+ };
239
232
 
240
- const items: ExportItem[] = [];
233
+ const buildItems = (block: TSESTree.ExportNamedDeclaration[]) =>
234
+ block
235
+ .map((node) => {
236
+ const name = getExportName(node);
237
+ if (!name) {
238
+ return null;
239
+ }
240
+ const item: ExportItem = {
241
+ isFunction: isFunctionExport(node),
242
+ name,
243
+ node,
244
+ text: sourceCode.getText(node)
245
+ };
246
+ return item;
247
+ })
248
+ .filter((item): item is ExportItem => item !== null);
249
+
250
+ const findFirstUnsorted = (items: ExportItem[]) => {
251
+ let messageId: MessageIds = "alphabetical";
241
252
 
242
- for (const node of block) {
243
- const name = getExportName(node);
244
- if (!name) {
245
- continue;
253
+ const unsorted = items.some((current, idx) => {
254
+ if (idx === 0) {
255
+ return false;
246
256
  }
257
+ const prev = items[idx - 1];
258
+ if (!prev) {
259
+ return false;
260
+ }
261
+ if (sortComparator(prev, current) <= 0) {
262
+ return false;
263
+ }
264
+ if (
265
+ variablesBeforeFunctions &&
266
+ prev.isFunction &&
267
+ !current.isFunction
268
+ ) {
269
+ messageId = "variablesBeforeFunctions";
270
+ }
271
+ return true;
272
+ });
247
273
 
248
- items.push({
249
- name,
250
- node,
251
- isFunction: isFunctionExport(node),
252
- text: sourceCode.getText(node)
253
- });
254
- }
274
+ return unsorted ? messageId : null;
275
+ };
255
276
 
256
- if (items.length < minKeys) {
277
+ const checkForwardDependencies = (items: ExportItem[]) => {
278
+ const exportNames = items.map((item) => item.name);
279
+ return items.some((item, idx) => {
280
+ const laterNames = new Set(exportNames.slice(idx + 1));
281
+ const nodeToCheck: TSESTree.Node =
282
+ item.node.declaration ?? item.node;
283
+ return hasForwardDependency(nodeToCheck, laterNames);
284
+ });
285
+ };
286
+
287
+ const processExportBlock = (
288
+ block: TSESTree.ExportNamedDeclaration[]
289
+ ) => {
290
+ if (block.length < minKeys) {
257
291
  return;
258
292
  }
259
293
 
260
- const sortedItems = items.slice().sort(sortComparator);
261
-
262
- let reportNeeded = false;
263
- let messageId: MessageIds = "alphabetical";
264
-
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;
271
- }
272
-
273
- if (sortComparator(prev, current) > 0) {
274
- reportNeeded = true;
294
+ const items = buildItems(block);
275
295
 
276
- if (
277
- variablesBeforeFunctions &&
278
- prev.isFunction &&
279
- !current.isFunction
280
- ) {
281
- messageId = "variablesBeforeFunctions";
282
- }
283
- break;
284
- }
296
+ if (items.length < minKeys) {
297
+ return;
285
298
  }
286
299
 
287
- if (!reportNeeded) {
300
+ const messageId = findFirstUnsorted(items);
301
+ if (!messageId) {
288
302
  return;
289
303
  }
290
304
 
291
- const exportNames = items.map((item) => item.name);
292
-
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;
301
-
302
- if (hasForwardDependency(nodeToCheck, laterNames)) {
303
- return;
304
- }
305
+ if (checkForwardDependencies(items)) {
306
+ return;
305
307
  }
306
308
 
309
+ const sortedItems = items.slice().sort(sortComparator);
310
+
307
311
  const expectedOrder = sortedItems
308
312
  .map((item) => item.name)
309
313
  .join(", ");
310
314
 
311
- const firstNode = block[0];
315
+ const [firstNode] = block;
312
316
  const lastNode = block[block.length - 1];
313
317
 
314
318
  if (!firstNode || !lastNode) {
@@ -316,49 +320,11 @@ export const sortExports: TSESLint.RuleModule<MessageIds, Options> = {
316
320
  }
317
321
 
318
322
  context.report({
319
- node: firstNode,
320
- messageId,
321
323
  data: {
322
324
  expectedOrder
323
325
  },
324
326
  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
- }
327
+ const fixableNodes = block.filter(isFixableExport);
362
328
 
363
329
  if (fixableNodes.length < minKeys) {
364
330
  return null;
@@ -368,8 +334,8 @@ export const sortExports: TSESLint.RuleModule<MessageIds, Options> = {
368
334
  .map((item) => generateExportText(item.node))
369
335
  .join("\n");
370
336
 
371
- const rangeStart = firstNode.range[0];
372
- const rangeEnd = lastNode.range[1];
337
+ const [rangeStart] = firstNode.range;
338
+ const [, rangeEnd] = lastNode.range;
373
339
 
374
340
  const fullText = sourceCode.getText();
375
341
  const originalText = fullText.slice(rangeStart, rangeEnd);
@@ -382,39 +348,77 @@ export const sortExports: TSESLint.RuleModule<MessageIds, Options> = {
382
348
  [rangeStart, rangeEnd],
383
349
  sortedText
384
350
  );
385
- }
351
+ },
352
+ messageId,
353
+ node: firstNode
386
354
  });
387
- }
355
+ };
388
356
 
389
357
  return {
390
358
  "Program:exit"(node: TSESTree.Program) {
391
- const body = node.body;
359
+ const { body } = node;
392
360
  const block: TSESTree.ExportNamedDeclaration[] = [];
393
361
 
394
- for (let i = 0; i < body.length; i++) {
395
- const stmt = body[i];
396
- if (!stmt) {
397
- continue;
398
- }
399
-
362
+ body.forEach((stmt) => {
400
363
  if (
401
364
  stmt.type === "ExportNamedDeclaration" &&
402
365
  !stmt.source &&
403
366
  getExportName(stmt) !== null
404
367
  ) {
405
368
  block.push(stmt);
406
- } else {
407
- if (block.length > 0) {
408
- processExportBlock(block);
409
- block.length = 0;
410
- }
369
+ return;
411
370
  }
412
- }
371
+
372
+ if (block.length > 0) {
373
+ processExportBlock(block);
374
+ block.length = 0;
375
+ }
376
+ });
413
377
 
414
378
  if (block.length > 0) {
415
379
  processExportBlock(block);
416
380
  }
417
381
  }
418
382
  };
383
+ },
384
+ defaultOptions: [{}],
385
+ meta: {
386
+ docs: {
387
+ description:
388
+ "Enforce that top-level export declarations are sorted by exported name and, optionally, that variable exports come before function exports"
389
+ },
390
+ fixable: "code",
391
+ messages: {
392
+ alphabetical:
393
+ "Export declarations are not sorted alphabetically. Expected order: {{expectedOrder}}.",
394
+ variablesBeforeFunctions:
395
+ "Non-function exports should come before function exports."
396
+ },
397
+ schema: [
398
+ {
399
+ additionalProperties: false,
400
+ properties: {
401
+ caseSensitive: {
402
+ type: "boolean"
403
+ },
404
+ minKeys: {
405
+ minimum: 2,
406
+ type: "integer"
407
+ },
408
+ natural: {
409
+ type: "boolean"
410
+ },
411
+ order: {
412
+ enum: ["asc", "desc"],
413
+ type: "string"
414
+ },
415
+ variablesBeforeFunctions: {
416
+ type: "boolean"
417
+ }
418
+ },
419
+ type: "object"
420
+ }
421
+ ],
422
+ type: "suggestion"
419
423
  }
420
424
  };