eslint-plugin-skuba 1.0.3-replace-global-vars-20251121010036 → 1.0.4-node-24-20251215224735

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/lib/index.cjs ADDED
@@ -0,0 +1,370 @@
1
+ let __typescript_eslint_utils = require("@typescript-eslint/utils");
2
+
3
+ //#region src/rules/no-sync-in-promise-iterable.ts
4
+ /**
5
+ * Whether a TypeScript type is a Promise-like "thenable".
6
+ */
7
+ const isThenableType = (type, checker) => {
8
+ if (type.symbol?.name === "Promise") return true;
9
+ const thenSymbol = type.getProperty("then");
10
+ if (thenSymbol?.valueDeclaration) {
11
+ if (checker.getTypeOfSymbolAtLocation(thenSymbol, thenSymbol.valueDeclaration).getCallSignatures().length) return true;
12
+ }
13
+ if (type.isUnion()) return type.types.every((t) => isThenableType(t, checker));
14
+ return false;
15
+ };
16
+ /**
17
+ * Whether a node represents a Promise-like "thenable".
18
+ */
19
+ const isThenableNode = (node, esTreeNodeToTSNodeMap, checker) => {
20
+ const tsNode = esTreeNodeToTSNodeMap.get(node);
21
+ return isThenableType(checker.getTypeAtLocation(tsNode), checker);
22
+ };
23
+ const isIterableType = (type, checker) => {
24
+ if (checker.isArrayLikeType(type)) return true;
25
+ return type.getProperties().some((property) => property.name.startsWith("__@iterator"));
26
+ };
27
+ const PROMISE_INSTANCE_METHODS = new Set([
28
+ "catch",
29
+ "then",
30
+ "finally"
31
+ ]);
32
+ const isChainedPromise = (node, esTreeNodeToTSNodeMap, checker) => {
33
+ if (!(node.callee.type === __typescript_eslint_utils.TSESTree.AST_NODE_TYPES.MemberExpression && node.callee.property.type === __typescript_eslint_utils.TSESTree.AST_NODE_TYPES.Identifier && PROMISE_INSTANCE_METHODS.has(node.callee.property.name))) return false;
34
+ const parent = esTreeNodeToTSNodeMap.get(node.callee.object);
35
+ return isThenableType(checker.getTypeAtLocation(parent), checker);
36
+ };
37
+ /**
38
+ * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects
39
+ */
40
+ const SAFE_ISH_CONSTRUCTORS = new Set([
41
+ "AggregateError",
42
+ "AsyncDisposableStack",
43
+ "Boolean",
44
+ "Date",
45
+ "DisposableStack",
46
+ "Error",
47
+ "EvalError",
48
+ "FinalizationRegistry",
49
+ "Map",
50
+ "Number",
51
+ "Object",
52
+ "Promise",
53
+ "Proxy",
54
+ "RangeError",
55
+ "ReferenceError",
56
+ "Set",
57
+ "SharedArrayBuffer",
58
+ "String",
59
+ "SuppressedError",
60
+ "Symbol",
61
+ "SyntaxError",
62
+ "TypeError",
63
+ "URIError",
64
+ "WeakMap",
65
+ "WeakRef",
66
+ "WeakSet"
67
+ ]);
68
+ const SAFE_ISH_FUNCTIONS = new Set([
69
+ "Boolean",
70
+ "isFinite",
71
+ "isNaN",
72
+ "Number",
73
+ "parseFloat",
74
+ "parseInt",
75
+ "String",
76
+ "Symbol"
77
+ ]);
78
+ const SAFE_ISH_STATIC_METHODS = {
79
+ Array: new Set([
80
+ "from",
81
+ "isArray",
82
+ "of"
83
+ ]),
84
+ Date: new Set([
85
+ "now",
86
+ "parse",
87
+ "UTC"
88
+ ]),
89
+ Iterator: new Set(["from"]),
90
+ Number: new Set([
91
+ "isFinite",
92
+ "isInteger",
93
+ "isNaN",
94
+ "isSafeInteger",
95
+ "parseFloat",
96
+ "parseInt"
97
+ ]),
98
+ Object: new Set([
99
+ "entries",
100
+ "fromEntries",
101
+ "groupBy",
102
+ "hasOwn",
103
+ "is",
104
+ "keys",
105
+ "values"
106
+ ]),
107
+ Promise: new Set([
108
+ "reject",
109
+ "resolve",
110
+ "try",
111
+ "withResolvers"
112
+ ])
113
+ };
114
+ const SAFE_ISH_INSTANCE_METHODS = new Set([
115
+ "apply",
116
+ "bind",
117
+ "call",
118
+ "toLocaleString",
119
+ "toString"
120
+ ]);
121
+ const isArrayFromAsync = (node) => node.callee.type === __typescript_eslint_utils.TSESTree.AST_NODE_TYPES.MemberExpression && node.callee.object.type === __typescript_eslint_utils.TSESTree.AST_NODE_TYPES.Identifier && node.callee.property.type === __typescript_eslint_utils.TSESTree.AST_NODE_TYPES.Identifier && node.callee.object.name === "Array" && node.callee.property.name === "fromAsync";
122
+ const isPromiseTry = (node) => node.callee.type === __typescript_eslint_utils.TSESTree.AST_NODE_TYPES.MemberExpression && node.callee.object.type === __typescript_eslint_utils.TSESTree.AST_NODE_TYPES.Identifier && node.callee.object.name === "Promise" && node.callee.property.type === __typescript_eslint_utils.TSESTree.AST_NODE_TYPES.Identifier && node.callee.property.name === "try";
123
+ /**
124
+ * Whether a call expression represents a safe-ish built-in.
125
+ *
126
+ * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects
127
+ */
128
+ const isSafeIshBuiltIn = (node) => {
129
+ if (node.callee.type === __typescript_eslint_utils.TSESTree.AST_NODE_TYPES.Identifier) return SAFE_ISH_FUNCTIONS.has(node.callee.name);
130
+ if (node.callee.type === __typescript_eslint_utils.TSESTree.AST_NODE_TYPES.MemberExpression) {
131
+ const { object, property } = node.callee;
132
+ if (object.type === __typescript_eslint_utils.TSESTree.AST_NODE_TYPES.Identifier && property.type === __typescript_eslint_utils.TSESTree.AST_NODE_TYPES.Identifier) return SAFE_ISH_STATIC_METHODS[object.name]?.has(property.name) ?? SAFE_ISH_INSTANCE_METHODS.has(property.name);
133
+ }
134
+ return false;
135
+ };
136
+ const SAFE_ISH_ITERABLE_INSTANCE_METHODS = new Set([
137
+ "concat",
138
+ "copyWithin",
139
+ "difference",
140
+ "drop",
141
+ "entries",
142
+ "every",
143
+ "fill",
144
+ "filter",
145
+ "find",
146
+ "findIndex",
147
+ "findLast",
148
+ "findLastIndex",
149
+ "flat",
150
+ "flatMap",
151
+ "has",
152
+ "includes",
153
+ "indexOf",
154
+ "intersection",
155
+ "keys",
156
+ "lastIndexOf",
157
+ "map",
158
+ "reduce",
159
+ "reduceRight",
160
+ "reverse",
161
+ "slice",
162
+ "some",
163
+ "splice",
164
+ "symmetricDifference",
165
+ "toArray",
166
+ "toReversed",
167
+ "toSorted",
168
+ "toSpliced",
169
+ "union",
170
+ "values",
171
+ "with"
172
+ ]);
173
+ /**
174
+ * Whether a call expression represents a method on an iterable object that is
175
+ * unlikely to internally throw a synchronous error.
176
+ */
177
+ const isSafeIshIterableMethod = (node, esTreeNodeToTSNodeMap, checker) => {
178
+ if (!(node.callee.type === __typescript_eslint_utils.TSESTree.AST_NODE_TYPES.MemberExpression && node.callee.property.type === __typescript_eslint_utils.TSESTree.AST_NODE_TYPES.Identifier && SAFE_ISH_ITERABLE_INSTANCE_METHODS.has(node.callee.property.name))) return false;
179
+ const tsNode = esTreeNodeToTSNodeMap.get(node.callee.object);
180
+ return isIterableType(checker.getTypeAtLocation(tsNode), checker);
181
+ };
182
+ /**
183
+ * Whether a call expression represents a safe-ish builder.
184
+ *
185
+ * This is currently overfitted to `knex`.
186
+ */
187
+ const isSafeIshBuilder = (node) => {
188
+ if (node.callee.type === __typescript_eslint_utils.TSESTree.AST_NODE_TYPES.MemberExpression && node.callee.object.type === __typescript_eslint_utils.TSESTree.AST_NODE_TYPES.CallExpression) return isSafeIshBuilder(node.callee.object);
189
+ if (node.callee.type === __typescript_eslint_utils.TSESTree.AST_NODE_TYPES.Identifier && node.callee.name === "knex" || node.callee.type === __typescript_eslint_utils.TSESTree.AST_NODE_TYPES.MemberExpression && node.callee.object.type === __typescript_eslint_utils.TSESTree.AST_NODE_TYPES.Identifier && node.callee.object.name === "knex") return true;
190
+ return false;
191
+ };
192
+ /**
193
+ * The nodes traversable from the current AST position containing logic with a
194
+ * reasonable chance of throwing a synchronous error.
195
+ */
196
+ const possibleNodesWithSyncError = (node, esTreeNodeToTSNodeMap, checker, sourceCode, visited, calls) => {
197
+ switch (node.type) {
198
+ case __typescript_eslint_utils.TSESTree.AST_NODE_TYPES.ArrayExpression: return node.elements.flatMap((arg) => arg ? possibleNodesWithSyncError(arg, esTreeNodeToTSNodeMap, checker, sourceCode, visited, calls) : []);
199
+ case __typescript_eslint_utils.TSESTree.AST_NODE_TYPES.ArrowFunctionExpression:
200
+ case __typescript_eslint_utils.TSESTree.AST_NODE_TYPES.FunctionExpression:
201
+ case __typescript_eslint_utils.TSESTree.AST_NODE_TYPES.FunctionDeclaration:
202
+ if (calls < 1) return [];
203
+ if (node.async) return [];
204
+ if (node.body.type === __typescript_eslint_utils.TSESTree.AST_NODE_TYPES.BlockStatement) return [node.parent];
205
+ return possibleNodesWithSyncError(node.body, esTreeNodeToTSNodeMap, checker, sourceCode, visited, calls - 1);
206
+ case __typescript_eslint_utils.TSESTree.AST_NODE_TYPES.AssignmentExpression: return possibleNodesWithSyncError(node.right, esTreeNodeToTSNodeMap, checker, sourceCode, visited, calls);
207
+ case __typescript_eslint_utils.TSESTree.AST_NODE_TYPES.BinaryExpression:
208
+ case __typescript_eslint_utils.TSESTree.AST_NODE_TYPES.LogicalExpression: return [node.left, node.right].flatMap((arg) => possibleNodesWithSyncError(arg, esTreeNodeToTSNodeMap, checker, sourceCode, visited, calls));
209
+ case __typescript_eslint_utils.TSESTree.AST_NODE_TYPES.CallExpression: {
210
+ if (isSafeIshBuilder(node)) return [];
211
+ if (isPromiseTry(node)) return node.arguments.flatMap((arg, index) => possibleNodesWithSyncError(arg, esTreeNodeToTSNodeMap, checker, sourceCode, visited, index === 0 ? calls : calls + 1));
212
+ if (isArrayFromAsync(node)) return node.arguments.flatMap((arg, index) => possibleNodesWithSyncError(arg, esTreeNodeToTSNodeMap, checker, sourceCode, visited, index === 1 ? calls : calls + 1));
213
+ if (isSafeIshBuiltIn(node)) return node.arguments.flatMap((arg) => possibleNodesWithSyncError(arg, esTreeNodeToTSNodeMap, checker, sourceCode, visited, calls + 1));
214
+ const collectCalleeArgumentErrors = (callee) => {
215
+ if (callee.type === __typescript_eslint_utils.TSESTree.AST_NODE_TYPES.CallExpression) {
216
+ const nestedErrors = collectCalleeArgumentErrors(callee.callee);
217
+ const argErrors = callee.arguments.flatMap((arg) => possibleNodesWithSyncError(arg, esTreeNodeToTSNodeMap, checker, sourceCode, visited, calls + 1));
218
+ return [...nestedErrors, ...argErrors];
219
+ }
220
+ return [];
221
+ };
222
+ const expression = node.callee.type === __typescript_eslint_utils.TSESTree.AST_NODE_TYPES.Identifier ? findExpression(node.callee, sourceCode, visited) : node.callee;
223
+ if (expression?.type === __typescript_eslint_utils.TSESTree.AST_NODE_TYPES.ArrowFunctionExpression || expression?.type === __typescript_eslint_utils.TSESTree.AST_NODE_TYPES.FunctionExpression) return [expression, ...node.arguments].flatMap((arg) => possibleNodesWithSyncError(arg, esTreeNodeToTSNodeMap, checker, sourceCode, visited, calls + 1));
224
+ const calleeErrors = collectCalleeArgumentErrors(node.callee);
225
+ if (isChainedPromise(node, esTreeNodeToTSNodeMap, checker)) return [...calleeErrors, ...node.arguments.flatMap((arg) => possibleNodesWithSyncError(arg, esTreeNodeToTSNodeMap, checker, sourceCode, visited, calls))];
226
+ if (isSafeIshIterableMethod(node, esTreeNodeToTSNodeMap, checker) || isThenableNode(node, esTreeNodeToTSNodeMap, checker)) return [...calleeErrors, ...node.arguments.flatMap((arg) => possibleNodesWithSyncError(arg, esTreeNodeToTSNodeMap, checker, sourceCode, visited, calls + 1))];
227
+ if (calleeErrors.length) return calleeErrors;
228
+ return [node];
229
+ }
230
+ case __typescript_eslint_utils.TSESTree.AST_NODE_TYPES.ChainExpression: return possibleNodesWithSyncError(node.expression, esTreeNodeToTSNodeMap, checker, sourceCode, visited, calls);
231
+ case __typescript_eslint_utils.TSESTree.AST_NODE_TYPES.ConditionalExpression: return [
232
+ node.test,
233
+ node.consequent,
234
+ node.alternate
235
+ ].flatMap((arg) => possibleNodesWithSyncError(arg, esTreeNodeToTSNodeMap, checker, sourceCode, visited, calls));
236
+ case __typescript_eslint_utils.TSESTree.AST_NODE_TYPES.Identifier: return [];
237
+ case __typescript_eslint_utils.TSESTree.AST_NODE_TYPES.Literal: return [];
238
+ case __typescript_eslint_utils.TSESTree.AST_NODE_TYPES.MemberExpression: return possibleNodesWithSyncError(node.object, esTreeNodeToTSNodeMap, checker, sourceCode, visited, calls);
239
+ case __typescript_eslint_utils.TSESTree.AST_NODE_TYPES.NewExpression:
240
+ if (node.callee.type === __typescript_eslint_utils.TSESTree.AST_NODE_TYPES.Identifier && node.callee.name === "Promise") return node.arguments.flatMap((arg, index) => possibleNodesWithSyncError(arg, esTreeNodeToTSNodeMap, checker, sourceCode, visited, index === 0 ? calls : calls + 1));
241
+ if (node.callee.type === __typescript_eslint_utils.TSESTree.AST_NODE_TYPES.Identifier && SAFE_ISH_CONSTRUCTORS.has(node.callee.name)) return node.arguments.flatMap((arg) => possibleNodesWithSyncError(arg, esTreeNodeToTSNodeMap, checker, sourceCode, visited, calls + 1));
242
+ return [node];
243
+ case __typescript_eslint_utils.TSESTree.AST_NODE_TYPES.SequenceExpression: return node.expressions.flatMap((arg) => possibleNodesWithSyncError(arg, esTreeNodeToTSNodeMap, checker, sourceCode, visited, calls));
244
+ case __typescript_eslint_utils.TSESTree.AST_NODE_TYPES.SpreadElement: return possibleNodesWithSyncError(node.argument, esTreeNodeToTSNodeMap, checker, sourceCode, visited, calls);
245
+ case __typescript_eslint_utils.TSESTree.AST_NODE_TYPES.TaggedTemplateExpression: return [node];
246
+ case __typescript_eslint_utils.TSESTree.AST_NODE_TYPES.TemplateLiteral: return node.expressions.flatMap((arg) => possibleNodesWithSyncError(arg, esTreeNodeToTSNodeMap, checker, sourceCode, visited, calls));
247
+ }
248
+ return [];
249
+ };
250
+ const getSourceCodeExcerpt = (node, sourceCode) => {
251
+ const text = sourceCode.getText(node);
252
+ const lines = text.split("\n");
253
+ return lines.length <= 2 ? text : `${lines[0]?.trimEnd()}...${lines[lines.length - 1]?.trimStart()}`;
254
+ };
255
+ const checkIterableForSyncErrors = (elements, method, context, esTreeNodeToTSNodeMap, checker) => {
256
+ for (const { element, reference } of elements) {
257
+ const nodes = possibleNodesWithSyncError(element, esTreeNodeToTSNodeMap, checker, context.sourceCode, /* @__PURE__ */ new Set(), 0);
258
+ for (const node of nodes) {
259
+ const root = reference ?? element;
260
+ const value = getSourceCodeExcerpt(root, context.sourceCode);
261
+ if (root.loc.start.column === node.loc.start.column && root.loc.start.line === node.loc.start.line) context.report({
262
+ node,
263
+ messageId: "mayThrowSyncError",
264
+ data: {
265
+ value,
266
+ method
267
+ }
268
+ });
269
+ else context.report({
270
+ node,
271
+ messageId: "mayLeadToSyncError",
272
+ data: {
273
+ value,
274
+ method,
275
+ underlying: getSourceCodeExcerpt(node, context.sourceCode),
276
+ line: node.loc.start.line.toString(),
277
+ column: node.loc.start.column.toString()
278
+ }
279
+ });
280
+ }
281
+ }
282
+ };
283
+ const findExpression = (node, sourceCode, visited) => {
284
+ let currentScope = sourceCode.getScope(node);
285
+ do {
286
+ const variable = currentScope.set.get(node.name);
287
+ const definition = variable?.defs[0];
288
+ if (!definition) continue;
289
+ if (visited.has(variable.name)) return;
290
+ visited.add(variable.name);
291
+ if (definition.node.type === __typescript_eslint_utils.TSESTree.AST_NODE_TYPES.VariableDeclarator && definition.node.init) return definition.node.init;
292
+ } while (currentScope = currentScope.upper);
293
+ };
294
+ const resolveArrayElements = (node, sourceCode, visited = /* @__PURE__ */ new Set(), reference) => {
295
+ switch (node.type) {
296
+ case __typescript_eslint_utils.TSESTree.AST_NODE_TYPES.ArrayExpression: return node.elements.flatMap((element, index) => {
297
+ if (!element) return [];
298
+ if (element.type === __typescript_eslint_utils.TSESTree.AST_NODE_TYPES.SpreadElement) return resolveArrayElements(element.argument, sourceCode, visited, reference ?? element);
299
+ if (index === 0) return [];
300
+ return {
301
+ element,
302
+ reference
303
+ };
304
+ });
305
+ case __typescript_eslint_utils.TSESTree.AST_NODE_TYPES.CallExpression: return [{
306
+ element: node,
307
+ reference
308
+ }];
309
+ case __typescript_eslint_utils.TSESTree.AST_NODE_TYPES.Identifier: {
310
+ const expression = findExpression(node, sourceCode, visited);
311
+ if (!expression) return [];
312
+ return resolveArrayElements(expression, sourceCode, visited, reference ?? node);
313
+ }
314
+ case __typescript_eslint_utils.TSESTree.AST_NODE_TYPES.SpreadElement: return resolveArrayElements(node.argument, sourceCode, visited, reference ?? node);
315
+ }
316
+ return [];
317
+ };
318
+ const createRule = __typescript_eslint_utils.ESLintUtils.RuleCreator((name) => `https://github.com/seek-oss/skuba/tree/main/docs/eslint-plugin/${name}.md`);
319
+ var no_sync_in_promise_iterable_default = createRule({
320
+ defaultOptions: [],
321
+ name: "no-sync-in-promise-iterable",
322
+ meta: {
323
+ type: "problem",
324
+ docs: {
325
+ description: "Heuristically flags synchronous logic in the iterable argument of static Promise methods that could leave preceding promises dangling",
326
+ recommended: true,
327
+ requiresTypeChecking: true
328
+ },
329
+ schema: [],
330
+ messages: {
331
+ mayLeadToSyncError: "{{value}} leads to {{underlying}} at {{line}}:{{column}} which may synchronously throw an error and leave preceding promises dangling. Evaluate synchronous expressions outside of the iterable argument to Promise.{{method}}, or safely wrap with the async keyword, Promise.try(), or Promise.resolve().then().",
332
+ mayThrowSyncError: "{{value}} may synchronously throw an error and leave preceding promises dangling. Evaluate synchronous expressions outside of the iterable argument to Promise.{{method}}, or safely wrap with the async keyword, Promise.try(), or Promise.resolve().then()."
333
+ }
334
+ },
335
+ create: (context) => {
336
+ const { esTreeNodeToTSNodeMap, program } = __typescript_eslint_utils.ESLintUtils.getParserServices(context);
337
+ const checker = program.getTypeChecker();
338
+ return { CallExpression: (node) => {
339
+ if (!(node.callee.type === __typescript_eslint_utils.TSESTree.AST_NODE_TYPES.MemberExpression && node.callee.object.type === __typescript_eslint_utils.TSESTree.AST_NODE_TYPES.Identifier && node.callee.object.name === "Promise" && node.callee.property.type === __typescript_eslint_utils.TSESTree.AST_NODE_TYPES.Identifier && [
340
+ "all",
341
+ "allSettled",
342
+ "any",
343
+ "race"
344
+ ].includes(node.callee.property.name))) return;
345
+ const method = node.callee.property.name;
346
+ const iterableArg = node.arguments[0];
347
+ if (!iterableArg) return;
348
+ checkIterableForSyncErrors(resolveArrayElements(iterableArg, context.sourceCode), method, context, esTreeNodeToTSNodeMap, checker);
349
+ } };
350
+ }
351
+ });
352
+
353
+ //#endregion
354
+ //#region src/index.ts
355
+ const skuba = {
356
+ meta: {
357
+ name: "skuba",
358
+ version: "1.0.0"
359
+ },
360
+ rules: { "no-sync-in-promise-iterable": no_sync_in_promise_iterable_default },
361
+ configs: {}
362
+ };
363
+ skuba.configs = { recommended: [{
364
+ plugins: { skuba },
365
+ rules: { "skuba/no-sync-in-promise-iterable": "warn" }
366
+ }] };
367
+ var src_default = skuba;
368
+
369
+ //#endregion
370
+ module.exports = src_default;