eslint-plugin-absolute 0.2.1 → 0.2.3

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/dist/index.js CHANGED
@@ -186,6 +186,75 @@ var explicitObjectTypes = {
186
186
 
187
187
  // src/rules/sort-keys-fixable.ts
188
188
  var SORT_BEFORE = -1;
189
+ var hasDuplicateNames = (names) => {
190
+ const seen = new Set;
191
+ const nonNullNames = names.flatMap((name) => name === null ? [] : [name]);
192
+ for (const name of nonNullNames) {
193
+ if (seen.has(name)) {
194
+ return true;
195
+ }
196
+ seen.add(name);
197
+ }
198
+ return false;
199
+ };
200
+ var isSafeStaticTemplate = (node) => node.expressions.length === 0;
201
+ var isSafeArrayElement = (node) => {
202
+ if (!node || node.type === "SpreadElement") {
203
+ return false;
204
+ }
205
+ return isSafeToReorderExpression(node);
206
+ };
207
+ var isSafeObjectProperty = (property) => {
208
+ if (property.type !== "Property" || property.computed || property.kind !== "init") {
209
+ return false;
210
+ }
211
+ if (property.key.type !== "Identifier" && property.key.type !== "Literal") {
212
+ return false;
213
+ }
214
+ if (property.method) {
215
+ return true;
216
+ }
217
+ return isSafeToReorderExpression(property.value);
218
+ };
219
+ var isSafeToReorderExpression = (node) => {
220
+ if (!node || node.type === "PrivateIdentifier") {
221
+ return false;
222
+ }
223
+ switch (node.type) {
224
+ case "Identifier":
225
+ case "Literal":
226
+ case "ThisExpression":
227
+ case "FunctionExpression":
228
+ case "ArrowFunctionExpression":
229
+ case "ClassExpression":
230
+ return true;
231
+ case "TemplateLiteral":
232
+ return isSafeStaticTemplate(node);
233
+ case "UnaryExpression":
234
+ return isSafeToReorderExpression(node.argument);
235
+ case "ArrayExpression":
236
+ return node.elements.every(isSafeArrayElement);
237
+ case "ObjectExpression":
238
+ return node.properties.every(isSafeObjectProperty);
239
+ default:
240
+ return false;
241
+ }
242
+ };
243
+ var isSafeJSXAttributeValue = (value) => {
244
+ if (value === null) {
245
+ return true;
246
+ }
247
+ if (value.type === "Literal") {
248
+ return true;
249
+ }
250
+ if (value.type !== "JSXExpressionContainer") {
251
+ return false;
252
+ }
253
+ if (value.expression.type === "JSXEmptyExpression") {
254
+ return false;
255
+ }
256
+ return isSafeToReorderExpression(value.expression);
257
+ };
189
258
  var sortKeysFixable = {
190
259
  create(context) {
191
260
  const { sourceCode } = context;
@@ -352,6 +421,12 @@ ${indent}`;
352
421
  node: prop
353
422
  };
354
423
  });
424
+ if (hasDuplicateNames(keys.map((key) => key.keyName))) {
425
+ autoFixable = false;
426
+ }
427
+ if (autoFixable && keys.some((key) => key.node.type === "Property" && !isSafeToReorderExpression(key.node.value))) {
428
+ autoFixable = false;
429
+ }
355
430
  let fixProvided = false;
356
431
  const createReportWithFix = (curr, shouldFix) => {
357
432
  context.report({
@@ -441,6 +516,20 @@ ${indent}`;
441
516
  if (!isOutOfOrder(names)) {
442
517
  return;
443
518
  }
519
+ if (hasDuplicateNames(names)) {
520
+ context.report({
521
+ messageId: "unsorted",
522
+ node: attrs[0].type === "JSXAttribute" ? attrs[0].name : attrs[0]
523
+ });
524
+ return;
525
+ }
526
+ if (attrs.some((attr) => attr.type === "JSXAttribute" && !isSafeJSXAttributeValue(attr.value))) {
527
+ context.report({
528
+ messageId: "unsorted",
529
+ node: attrs[0].type === "JSXAttribute" ? attrs[0].name : attrs[0]
530
+ });
531
+ return;
532
+ }
444
533
  const braceConflict = attrs.find((currAttr, idx) => {
445
534
  if (idx === 0) {
446
535
  return false;
@@ -797,7 +886,26 @@ var noUnnecessaryKey = {
797
886
  };
798
887
 
799
888
  // src/rules/sort-exports.ts
800
- var SORT_BEFORE2 = -1;
889
+ var SORT_BEFORE2 = Number.parseInt("-1", 10);
890
+ var hasStringTypeProperty = (value) => {
891
+ const maybeType = Reflect.get(value, "type");
892
+ return typeof maybeType === "string";
893
+ };
894
+ var isNodeLike = (value) => value !== null && value !== undefined && typeof value === "object" && ("type" in value) && hasStringTypeProperty(value);
895
+ var shouldSkipNodeEntry = (key, value) => key === "parent" || value === null || value === undefined;
896
+ var visitNodeArray = (values, visit) => values.filter(isNodeLike).forEach(visit);
897
+ var visitNodeEntryValue = (value, visit) => {
898
+ if (Array.isArray(value)) {
899
+ visitNodeArray(value, visit);
900
+ return;
901
+ }
902
+ if (isNodeLike(value)) {
903
+ visit(value);
904
+ }
905
+ };
906
+ var visitNodeEntries = (current, visit) => Object.entries(current).filter(([key, value]) => !shouldSkipNodeEntry(key, value)).forEach(([, value]) => {
907
+ visitNodeEntryValue(value, visit);
908
+ });
801
909
  var getVariableDeclaratorName = (declaration) => {
802
910
  if (declaration.declarations.length !== 1) {
803
911
  return null;
@@ -848,6 +956,76 @@ var isFixableExport = (exportNode) => {
848
956
  }
849
957
  return (declaration.type === "FunctionDeclaration" || declaration.type === "ClassDeclaration") && declaration.id !== null && declaration.id.type === "Identifier";
850
958
  };
959
+ var visitImmediateReferences = (node, onReference) => {
960
+ if (!node) {
961
+ return;
962
+ }
963
+ const visit = (current) => {
964
+ if (!current) {
965
+ return;
966
+ }
967
+ switch (current.type) {
968
+ case "Identifier":
969
+ onReference(current.name);
970
+ return;
971
+ case "FunctionDeclaration":
972
+ case "FunctionExpression":
973
+ case "ArrowFunctionExpression":
974
+ return;
975
+ case "MemberExpression":
976
+ visit(current.object);
977
+ if (current.computed) {
978
+ visit(current.property);
979
+ }
980
+ return;
981
+ case "Property":
982
+ if (current.computed) {
983
+ visit(current.key);
984
+ }
985
+ visit(current.value);
986
+ return;
987
+ case "PropertyDefinition":
988
+ if (current.computed) {
989
+ visit(current.key);
990
+ }
991
+ if (current.static) {
992
+ visit(current.value);
993
+ }
994
+ return;
995
+ case "MethodDefinition":
996
+ if (current.computed) {
997
+ visit(current.key);
998
+ }
999
+ return;
1000
+ case "StaticBlock":
1001
+ for (const statement of current.body) {
1002
+ visit(statement);
1003
+ }
1004
+ return;
1005
+ }
1006
+ visitNodeEntries(current, visit);
1007
+ };
1008
+ visit(node);
1009
+ };
1010
+ var getImmediateDependencyNames = (node) => {
1011
+ const names = new Set;
1012
+ const { declaration } = node;
1013
+ const addName = names.add.bind(names);
1014
+ const addDeclaratorDependencies = (declarator) => visitImmediateReferences(declarator.init, addName);
1015
+ const addClassElementDependencies = (element) => visitImmediateReferences(element, addName);
1016
+ if (!declaration) {
1017
+ return names;
1018
+ }
1019
+ if (declaration.type === "VariableDeclaration") {
1020
+ declaration.declarations.forEach(addDeclaratorDependencies);
1021
+ return names;
1022
+ }
1023
+ if (declaration.type === "ClassDeclaration") {
1024
+ visitImmediateReferences(declaration.superClass, addName);
1025
+ declaration.body.body.forEach(addClassElementDependencies);
1026
+ }
1027
+ return names;
1028
+ };
851
1029
  var sortExports = {
852
1030
  create(context) {
853
1031
  const { sourceCode } = context;
@@ -903,15 +1081,6 @@ var sortExports = {
903
1081
  }
904
1082
  return compareStrings(left.name, right.name);
905
1083
  };
906
- const hasForwardDependency = (node, laterNames) => {
907
- const text = sourceCode.getText(node);
908
- for (const name of laterNames) {
909
- if (text.includes(name)) {
910
- return true;
911
- }
912
- }
913
- return false;
914
- };
915
1084
  const buildItems = (block) => block.map((node) => {
916
1085
  const name = getExportName(node);
917
1086
  if (!name) {
@@ -945,12 +1114,41 @@ var sortExports = {
945
1114
  });
946
1115
  return unsorted ? messageId : null;
947
1116
  };
948
- const checkForwardDependencies = (items) => {
1117
+ const hasForwardDependenciesInOrder = (items) => {
949
1118
  const exportNames = items.map((item) => item.name);
950
1119
  return items.some((item, idx) => {
951
1120
  const laterNames = new Set(exportNames.slice(idx + 1));
952
- const nodeToCheck = item.node.declaration ?? item.node;
953
- return hasForwardDependency(nodeToCheck, laterNames);
1121
+ if (laterNames.size === 0) {
1122
+ return false;
1123
+ }
1124
+ const dependencies = getImmediateDependencyNames(item.node);
1125
+ for (const dependency of dependencies) {
1126
+ if (laterNames.has(dependency)) {
1127
+ return true;
1128
+ }
1129
+ }
1130
+ return false;
1131
+ });
1132
+ };
1133
+ const wouldCreateForwardDependencies = (items, sortedItems) => {
1134
+ const sortedIndices = new Map(sortedItems.map((item, idx) => [item.name, idx]));
1135
+ const exportNames = new Set(items.map((item) => item.name));
1136
+ return items.some((item) => {
1137
+ const itemIndex = sortedIndices.get(item.name);
1138
+ if (itemIndex === undefined) {
1139
+ return false;
1140
+ }
1141
+ const dependencies = getImmediateDependencyNames(item.node);
1142
+ for (const dependency of dependencies) {
1143
+ if (!exportNames.has(dependency)) {
1144
+ continue;
1145
+ }
1146
+ const dependencyIndex = sortedIndices.get(dependency);
1147
+ if (dependencyIndex !== undefined && itemIndex < dependencyIndex) {
1148
+ return true;
1149
+ }
1150
+ }
1151
+ return false;
954
1152
  });
955
1153
  };
956
1154
  const processExportBlock = (block) => {
@@ -965,10 +1163,13 @@ var sortExports = {
965
1163
  if (!messageId) {
966
1164
  return;
967
1165
  }
968
- if (checkForwardDependencies(items)) {
1166
+ if (hasForwardDependenciesInOrder(items)) {
969
1167
  return;
970
1168
  }
971
1169
  const sortedItems = items.slice().sort(sortComparator);
1170
+ if (wouldCreateForwardDependencies(items, sortedItems)) {
1171
+ return;
1172
+ }
972
1173
  const expectedOrder = sortedItems.map((item) => item.name).join(", ");
973
1174
  const [firstNode] = block;
974
1175
  const lastNode = block[block.length - 1];
package/eslint.config.mjs CHANGED
@@ -30,7 +30,7 @@ export default defineConfig([
30
30
  }
31
31
  },
32
32
  {
33
- files: ["**/*.{ts,js,mjs}"],
33
+ files: ["**/*.{ts,js,mjs,json}"],
34
34
  plugins: {
35
35
  absolute: absolutePlugin
36
36
  },
@@ -92,7 +92,7 @@ export default defineConfig([
92
92
  }
93
93
  },
94
94
  {
95
- files: ["eslint.config.mjs"],
95
+ files: ["eslint.config.mjs", "package.json", "tsconfig.json"],
96
96
  rules: {
97
97
  "@typescript-eslint/no-unused-expressions": "off"
98
98
  }
package/package.json CHANGED
@@ -1,27 +1,9 @@
1
1
  {
2
- "name": "eslint-plugin-absolute",
3
- "version": "0.2.1",
4
- "description": "ESLint plugin for AbsoluteJS",
5
- "repository": {
6
- "type": "git",
7
- "url": "https://github.com/absolutejs/eslint-plugin-absolute.git"
8
- },
9
- "type": "module",
10
- "main": "./dist/index.js",
11
- "license": "CC BY-NC 4.0",
12
2
  "author": "Alex Kahn",
13
- "scripts": {
14
- "test": "bun test tests/",
15
- "build": "rm -rf dist && bun build src/index.ts --outdir dist --splitting --target=bun --external eslint --external @typescript-eslint/utils",
16
- "format": "absolutejs prettier --write",
17
- "release": "bun run format && bun run build && bun publish",
18
- "prune": "ts-prune --error",
19
- "lint": "bun run build && bun run absolutejs eslint",
20
- "typecheck": "bun run tsc --noEmit"
21
- },
3
+ "description": "ESLint plugin for AbsoluteJS",
22
4
  "devDependencies": {
23
- "@types/bun": "1.3.3",
24
5
  "@absolutejs/absolute": "0.16.10",
6
+ "@types/bun": "1.3.3",
25
7
  "@types/react": "19.2.14",
26
8
  "@typescript-eslint/rule-tester": "8.56.0",
27
9
  "@typescript-eslint/utils": "8.56.0",
@@ -30,5 +12,23 @@
30
12
  "ts-prune": "0.10.3",
31
13
  "typescript": "5.9.3",
32
14
  "typescript-eslint": "8.56.0"
33
- }
15
+ },
16
+ "license": "CC BY-NC 4.0",
17
+ "main": "./dist/index.js",
18
+ "name": "eslint-plugin-absolute",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "https://github.com/absolutejs/eslint-plugin-absolute.git"
22
+ },
23
+ "scripts": {
24
+ "build": "rm -rf dist && bun build src/index.ts --outdir dist --splitting --target=bun --external eslint --external @typescript-eslint/utils",
25
+ "format": "absolutejs prettier --write",
26
+ "lint": "bun run build && bun run absolutejs eslint",
27
+ "prune": "ts-prune --error",
28
+ "release": "bun run format && bun run build && bun publish",
29
+ "test": "bun test tests/",
30
+ "typecheck": "bun run tsc --noEmit"
31
+ },
32
+ "type": "module",
33
+ "version": "0.2.3"
34
34
  }
@@ -30,7 +30,51 @@ type ExportItem = {
30
30
  text: string;
31
31
  };
32
32
 
33
- const SORT_BEFORE: -1 = -1;
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
+ });
34
78
 
35
79
  const getVariableDeclaratorName = (
36
80
  declaration: TSESTree.VariableDeclaration
@@ -114,6 +158,93 @@ const isFixableExport = (exportNode: TSESTree.ExportNamedDeclaration) => {
114
158
  );
115
159
  };
116
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
+
117
248
  export const sortExports: TSESLint.RuleModule<MessageIds, Options> = {
118
249
  create(context) {
119
250
  const { sourceCode } = context;
@@ -213,23 +344,6 @@ export const sortExports: TSESLint.RuleModule<MessageIds, Options> = {
213
344
  return compareStrings(left.name, right.name);
214
345
  };
215
346
 
216
- /**
217
- * Very lightweight dependency check: look at the text of the node and see
218
- * if it references any of the later export names.
219
- */
220
- const hasForwardDependency = (
221
- node: TSESTree.Node,
222
- laterNames: Set<string>
223
- ) => {
224
- const text = sourceCode.getText(node);
225
- for (const name of laterNames) {
226
- if (text.includes(name)) {
227
- return true;
228
- }
229
- }
230
- return false;
231
- };
232
-
233
347
  const buildItems = (block: TSESTree.ExportNamedDeclaration[]) =>
234
348
  block
235
349
  .map((node) => {
@@ -274,13 +388,52 @@ export const sortExports: TSESLint.RuleModule<MessageIds, Options> = {
274
388
  return unsorted ? messageId : null;
275
389
  };
276
390
 
277
- const checkForwardDependencies = (items: ExportItem[]) => {
391
+ const hasForwardDependenciesInOrder = (items: ExportItem[]) => {
278
392
  const exportNames = items.map((item) => item.name);
279
393
  return items.some((item, idx) => {
280
394
  const laterNames = new Set(exportNames.slice(idx + 1));
281
- const nodeToCheck: TSESTree.Node =
282
- item.node.declaration ?? item.node;
283
- return hasForwardDependency(nodeToCheck, laterNames);
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
+ if (!exportNames.has(dependency)) {
427
+ continue;
428
+ }
429
+
430
+ const dependencyIndex = sortedIndices.get(dependency);
431
+ if (dependencyIndex !== undefined && itemIndex < dependencyIndex) {
432
+ return true;
433
+ }
434
+ }
435
+
436
+ return false;
284
437
  });
285
438
  };
286
439
 
@@ -302,12 +455,16 @@ export const sortExports: TSESLint.RuleModule<MessageIds, Options> = {
302
455
  return;
303
456
  }
304
457
 
305
- if (checkForwardDependencies(items)) {
458
+ if (hasForwardDependenciesInOrder(items)) {
306
459
  return;
307
460
  }
308
461
 
309
462
  const sortedItems = items.slice().sort(sortComparator);
310
463
 
464
+ if (wouldCreateForwardDependencies(items, sortedItems)) {
465
+ return;
466
+ }
467
+
311
468
  const expectedOrder = sortedItems
312
469
  .map((item) => item.name)
313
470
  .join(", ");