@logtape/lint 2.2.0-dev.0

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.
Files changed (54) hide show
  1. package/LICENSE +20 -0
  2. package/dist/core/ast.cjs +517 -0
  3. package/dist/core/ast.js +506 -0
  4. package/dist/core/ast.js.map +1 -0
  5. package/dist/deno/plugin.cjs +332 -0
  6. package/dist/deno/plugin.d.cts +46 -0
  7. package/dist/deno/plugin.d.cts.map +1 -0
  8. package/dist/deno/plugin.d.ts +46 -0
  9. package/dist/deno/plugin.d.ts.map +1 -0
  10. package/dist/deno/plugin.js +333 -0
  11. package/dist/deno/plugin.js.map +1 -0
  12. package/dist/eslint/plugin.cjs +51 -0
  13. package/dist/eslint/plugin.d.cts +26 -0
  14. package/dist/eslint/plugin.d.cts.map +1 -0
  15. package/dist/eslint/plugin.d.ts +26 -0
  16. package/dist/eslint/plugin.d.ts.map +1 -0
  17. package/dist/eslint/plugin.js +44 -0
  18. package/dist/eslint/plugin.js.map +1 -0
  19. package/dist/mod.cjs +15 -0
  20. package/dist/mod.d.cts +6 -0
  21. package/dist/mod.d.ts +6 -0
  22. package/dist/mod.js +7 -0
  23. package/dist/rules/no-message-interpolation.cjs +52 -0
  24. package/dist/rules/no-message-interpolation.d.cts +23 -0
  25. package/dist/rules/no-message-interpolation.d.cts.map +1 -0
  26. package/dist/rules/no-message-interpolation.d.ts +23 -0
  27. package/dist/rules/no-message-interpolation.d.ts.map +1 -0
  28. package/dist/rules/no-message-interpolation.js +53 -0
  29. package/dist/rules/no-message-interpolation.js.map +1 -0
  30. package/dist/rules/no-unawaited-log.cjs +67 -0
  31. package/dist/rules/no-unawaited-log.d.cts +21 -0
  32. package/dist/rules/no-unawaited-log.d.cts.map +1 -0
  33. package/dist/rules/no-unawaited-log.d.ts +21 -0
  34. package/dist/rules/no-unawaited-log.d.ts.map +1 -0
  35. package/dist/rules/no-unawaited-log.js +68 -0
  36. package/dist/rules/no-unawaited-log.js.map +1 -0
  37. package/dist/rules/prefer-lazy-evaluation.cjs +59 -0
  38. package/dist/rules/prefer-lazy-evaluation.d.cts +22 -0
  39. package/dist/rules/prefer-lazy-evaluation.d.cts.map +1 -0
  40. package/dist/rules/prefer-lazy-evaluation.d.ts +22 -0
  41. package/dist/rules/prefer-lazy-evaluation.d.ts.map +1 -0
  42. package/dist/rules/prefer-lazy-evaluation.js +60 -0
  43. package/dist/rules/prefer-lazy-evaluation.js.map +1 -0
  44. package/dist/rules/require-meta-sink.cjs +75 -0
  45. package/dist/rules/require-meta-sink.d.cts +27 -0
  46. package/dist/rules/require-meta-sink.d.cts.map +1 -0
  47. package/dist/rules/require-meta-sink.d.ts +27 -0
  48. package/dist/rules/require-meta-sink.d.ts.map +1 -0
  49. package/dist/rules/require-meta-sink.js +76 -0
  50. package/dist/rules/require-meta-sink.js.map +1 -0
  51. package/dist/utils.cjs +82 -0
  52. package/dist/utils.js +82 -0
  53. package/dist/utils.js.map +1 -0
  54. package/package.json +90 -0
package/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Copyright 2024–2026 Hong Minhee
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,517 @@
1
+
2
+ //#region src/core/ast.ts
3
+ /**
4
+ * Shared, host-agnostic AST analysis used by both the ESLint rules
5
+ * (`../rules/`) and the Deno Lint plugin (`../deno/plugin.ts`).
6
+ *
7
+ * Every function here works on plain ESTree-shaped AST nodes and depends only
8
+ * on `node.type` / `node.parent` style traversal, so the same logic runs under
9
+ * ESLint, Oxlint, and Deno Lint. Divergences between the host ASTs (e.g. Deno
10
+ * exposing `TemplateElement.cooked` directly where ESTree nests it under
11
+ * `value.cooked`) are absorbed here with `??` fallbacks, so an edge case is
12
+ * fixed once rather than in two drifting copies.
13
+ *
14
+ * This module must not import from `eslint`, not even types: the Deno plugin
15
+ * imports it and must stay loadable without the ESLint package present.
16
+ *
17
+ * @module
18
+ */
19
+ /**
20
+ * Log method names that the LogTape lint rules check.
21
+ */
22
+ const LOG_METHODS = new Set([
23
+ "trace",
24
+ "debug",
25
+ "info",
26
+ "warn",
27
+ "warning",
28
+ "error",
29
+ "fatal"
30
+ ]);
31
+ /**
32
+ * AST node types that introduce their own function scope.
33
+ */
34
+ const ASYNC_FUNCTION_TYPES = new Set([
35
+ "ArrowFunctionExpression",
36
+ "FunctionExpression",
37
+ "FunctionDeclaration"
38
+ ]);
39
+ /**
40
+ * Maximum depth for the recursive AST scans. The AST is finite, so this is a
41
+ * safety net against pathological nesting rather than an expected limit; it is
42
+ * generous enough to cover deeply nested log property objects (complex API
43
+ * payloads, ORM entities) without false negatives.
44
+ */
45
+ const MAX_RECURSION_DEPTH = 100;
46
+ /**
47
+ * Whether an import source refers to the LogTape core package. Accepts the
48
+ * bare specifier (`@logtape/logtape`) as well as direct Deno-style `jsr:` and
49
+ * `npm:` specifiers with an optional version suffix (e.g.
50
+ * `jsr:@logtape/logtape` or `npm:@logtape/logtape@^1.0.0`).
51
+ */
52
+ function isLogtapeImportSource(source) {
53
+ return typeof source === "string" && /^(?:(?:jsr|npm):)?@logtape\/logtape(?:@[^/]+)?$/.test(source);
54
+ }
55
+ /**
56
+ * Unwrap TypeScript type-assertion wrappers around an expression (`x as T`,
57
+ * `<T>x`, `x satisfies T`, `x!`) so the rules analyze the underlying node.
58
+ * Each wrapper exposes the inner node as `.expression`. Returns the node
59
+ * unchanged when it is not such a wrapper.
60
+ */
61
+ function unwrapTypeAssertion(node) {
62
+ while (node && (node.type === "TSAsExpression" || node.type === "TSTypeAssertion" || node.type === "TSSatisfiesExpression" || node.type === "TSNonNullExpression")) node = node.expression;
63
+ return node;
64
+ }
65
+ /**
66
+ * Resolve the log method name from a member-expression callee
67
+ * (`logger.debug` -> `"debug"`), supporting computed string-literal access
68
+ * (`logger["debug"]`). Returns `null` for a computed non-literal property or a
69
+ * non-member callee.
70
+ */
71
+ function logMethodName(callee) {
72
+ if (!callee || callee.type !== "MemberExpression") return null;
73
+ if (!callee.computed) return callee.property?.name ?? null;
74
+ return callee.property?.type === "Literal" && typeof callee.property.value === "string" ? callee.property.value : null;
75
+ }
76
+ /**
77
+ * Recursively check whether an AST node contains an eagerly evaluated call
78
+ * anywhere in its subtree. Only inspects own-enumerable child nodes, skipping
79
+ * metadata fields. A LogTape `lazy(...)` call (whose local names are in
80
+ * `lazyNames`) is already deferred, so it is not counted as eager; its
81
+ * arguments are still inspected, so `lazy(expensive())` is caught while
82
+ * `lazy(() => expensive())` is not.
83
+ */
84
+ function containsCallExpression(node, lazyNames, depth = 0) {
85
+ if (depth > MAX_RECURSION_DEPTH || !node || typeof node !== "object") return false;
86
+ const isLazyCall = node.type === "CallExpression" && node.callee?.type === "Identifier" && lazyNames.has(node.callee.name);
87
+ if (!isLazyCall && (node.type === "CallExpression" || node.type === "OptionalCallExpression" || node.type === "NewExpression" || node.type === "TaggedTemplateExpression" || node.type === "ImportExpression")) return true;
88
+ if (node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression" || node.type === "FunctionDeclaration") return false;
89
+ for (const key in node) {
90
+ if (key === "type" || key === "start" || key === "end" || key === "loc" || key === "parent" || key === "range") continue;
91
+ const child = node[key];
92
+ if (Array.isArray(child)) {
93
+ for (const item of child) if (typeof item === "object" && item !== null && containsCallExpression(item, lazyNames, depth + 1)) return true;
94
+ } else if (typeof child === "object" && child !== null) {
95
+ if (containsCallExpression(child, lazyNames, depth + 1)) return true;
96
+ }
97
+ }
98
+ return false;
99
+ }
100
+ /**
101
+ * Recursively check whether a node contains an `AwaitExpression` or
102
+ * `YieldExpression` in the same function scope. Does not descend into nested
103
+ * function bodies.
104
+ */
105
+ function containsAwaitOrYield(node, depth = 0) {
106
+ if (depth > MAX_RECURSION_DEPTH || !node || typeof node !== "object") return false;
107
+ if (node.type === "AwaitExpression" || node.type === "YieldExpression") return true;
108
+ if (node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression" || node.type === "FunctionDeclaration") return false;
109
+ for (const key in node) {
110
+ if (key === "type" || key === "start" || key === "end" || key === "loc" || key === "parent" || key === "range") continue;
111
+ const child = node[key];
112
+ if (Array.isArray(child)) {
113
+ for (const item of child) if (typeof item === "object" && item !== null && containsAwaitOrYield(item, depth + 1)) return true;
114
+ } else if (typeof child === "object" && child !== null) {
115
+ if (containsAwaitOrYield(child, depth + 1)) return true;
116
+ }
117
+ }
118
+ return false;
119
+ }
120
+ /**
121
+ * From a log call's argument list, select the eager properties object and note
122
+ * whether it came from the properties-only overload. The properties object is
123
+ * the second argument in the message+properties form
124
+ * (`logger.debug("msg", { ... })`) or the first argument in the
125
+ * properties-only form (`logger.debug({ ... })`). TypeScript type assertions
126
+ * (e.g. `{ ... } as const`) are unwrapped first. Returns `null` when neither
127
+ * argument is an object literal.
128
+ *
129
+ * `propsObject` is the unwrapped object (for detection and the report
130
+ * location); `fixTarget` is the original, still-wrapped argument node, so the
131
+ * autofix replaces the whole `{ ... } as const` rather than leaving the
132
+ * assertion dangling on the new callback. When the argument is not wrapped the
133
+ * two are the same node.
134
+ */
135
+ function selectLazyPropsObject(args) {
136
+ const firstRaw = args?.[0];
137
+ const secondRaw = args?.[1];
138
+ const firstArg = unwrapTypeAssertion(firstRaw);
139
+ const secondArg = unwrapTypeAssertion(secondRaw);
140
+ if (firstArg && firstArg.type === "ObjectExpression") return {
141
+ propsObject: firstArg,
142
+ fixTarget: firstRaw,
143
+ propertiesOnly: true
144
+ };
145
+ if (secondArg && secondArg.type === "ObjectExpression") return {
146
+ propsObject: secondArg,
147
+ fixTarget: secondRaw,
148
+ propertiesOnly: false
149
+ };
150
+ return null;
151
+ }
152
+ /**
153
+ * Whether any property (or spread) of a properties object contains an eager
154
+ * call that would benefit from lazy evaluation.
155
+ */
156
+ function propsHaveEagerCall(propsObject, lazyNames) {
157
+ return propsObject.properties?.some((prop) => prop.type === "Property" && (containsCallExpression(prop.value, lazyNames) || prop.computed && containsCallExpression(prop.key, lazyNames)) || prop.type === "SpreadElement" && containsCallExpression(prop.argument, lazyNames)) ?? false;
158
+ }
159
+ /**
160
+ * Whether `node` is an async function literal (arrow or function expression).
161
+ */
162
+ function isAsyncFunctionExpr(node) {
163
+ return (node?.type === "ArrowFunctionExpression" || node?.type === "FunctionExpression") && node.async === true;
164
+ }
165
+ /**
166
+ * Walk up the parent chain to find the nearest enclosing function. Returns
167
+ * `null` if the top of the tree is reached without finding one.
168
+ */
169
+ function findEnclosingFunction(node) {
170
+ let current = node.parent;
171
+ while (current) {
172
+ if (ASYNC_FUNCTION_TYPES.has(current.type)) return current;
173
+ current = current.parent;
174
+ }
175
+ return null;
176
+ }
177
+ /**
178
+ * Whether `fn` is a function passed directly as a call argument (e.g. an array
179
+ * `.map()`/`.forEach()` callback). Such a function's return value is decided
180
+ * by the receiving call, so a promise it returns may be awaited or discarded.
181
+ */
182
+ function isCallArgumentFunction(fn) {
183
+ const parent = fn?.parent;
184
+ return parent?.type === "CallExpression" && parent.callee !== fn && (parent.arguments?.includes(fn) ?? false);
185
+ }
186
+ /**
187
+ * Array iteration methods that ignore, coerce, or merely thread the callback's
188
+ * return value, so a promise returned from the callback is not awaited per
189
+ * call. `reduce`/`reduceRight` pass it on as the next accumulator (only the
190
+ * final accumulator is the result), so earlier per-item promises are dropped.
191
+ */
192
+ const DISCARDING_ARRAY_METHODS = new Set([
193
+ "forEach",
194
+ "filter",
195
+ "find",
196
+ "findLast",
197
+ "findIndex",
198
+ "findLastIndex",
199
+ "some",
200
+ "every",
201
+ "reduce",
202
+ "reduceRight"
203
+ ]);
204
+ /**
205
+ * Global "fire and forget" functions that ignore their callback's return value
206
+ * entirely, so a promise returned from the callback is never awaited.
207
+ */
208
+ const FIRE_AND_FORGET_GLOBALS = new Set([
209
+ "setTimeout",
210
+ "setInterval",
211
+ "setImmediate",
212
+ "queueMicrotask",
213
+ "requestAnimationFrame",
214
+ "requestIdleCallback"
215
+ ]);
216
+ /**
217
+ * Whether `fn` is a callback argument to a call that ignores or coerces its
218
+ * callback's return value: an array method like `forEach`/`filter`/`some`, or a
219
+ * fire-and-forget global like `setTimeout`/`queueMicrotask`. Such a call does
220
+ * not propagate the callback's promise, so an async log returned from it is
221
+ * dropped and the walk must stop there rather than continue to an outer
222
+ * await/return.
223
+ */
224
+ function isDiscardedCallbackArgument(fn) {
225
+ const parent = fn?.parent;
226
+ if (!parent || parent.type !== "CallExpression") return false;
227
+ if (parent.callee === fn) return false;
228
+ if (!(parent.arguments?.includes(fn) ?? false)) return false;
229
+ const callee = parent.callee;
230
+ if (callee?.type === "Identifier" && FIRE_AND_FORGET_GLOBALS.has(callee.name)) return true;
231
+ const methodName = logMethodName(callee);
232
+ return methodName !== null && DISCARDING_ARRAY_METHODS.has(methodName);
233
+ }
234
+ /**
235
+ * Whether `node` is the result of a `map()`/`flatMap()` call. Such a call
236
+ * returns an array of the callback's return values, so awaiting or returning it
237
+ * does not await the element promises; only a Promise combinator
238
+ * (`Promise.all`/`allSettled`) awaits those.
239
+ */
240
+ function isMapResult(node) {
241
+ return node?.type === "CallExpression" && node.callee?.type === "MemberExpression" && !node.callee.computed && node.callee.property?.type === "Identifier" && (node.callee.property.name === "map" || node.callee.property.name === "flatMap");
242
+ }
243
+ /**
244
+ * Array methods that return a new array holding the same elements, so a
245
+ * `map()`/`flatMap()` result chained through them is still an array of the
246
+ * original promises (e.g. `arr.map(cb).filter(Boolean)`). Methods that unwrap
247
+ * to a single element (`find`, `at`) or transform the elements (`map`,
248
+ * `reduce`) are deliberately excluded.
249
+ */
250
+ const ELEMENT_PRESERVING_ARRAY_METHODS = new Set([
251
+ "filter",
252
+ "slice",
253
+ "concat",
254
+ "flat",
255
+ "reverse",
256
+ "sort",
257
+ "toSorted",
258
+ "toReversed",
259
+ "with"
260
+ ]);
261
+ /**
262
+ * Whether `node` evaluates to an array of the promises produced by a
263
+ * `map()`/`flatMap()`, either directly or chained through element-preserving
264
+ * array methods (`arr.map(cb).filter(...).slice(...)`). Awaiting or returning
265
+ * such an array awaits the array, not the element promises.
266
+ */
267
+ function isMapResultChain(node, depth = 0) {
268
+ if (depth > MAX_RECURSION_DEPTH || !node) return false;
269
+ if (isMapResult(node)) return true;
270
+ if (node.type === "CallExpression" && node.callee?.type === "MemberExpression" && !node.callee.computed && node.callee.property?.type === "Identifier" && ELEMENT_PRESERVING_ARRAY_METHODS.has(node.callee.property.name)) return isMapResultChain(node.callee.object, depth + 1);
271
+ return false;
272
+ }
273
+ /**
274
+ * Whether `node` is an expression that is syntactically a promise. Recognizes
275
+ * `x.then(...)` / `.catch(...)` / `.finally(...)`, `new Promise(...)`, and
276
+ * `Promise.resolve/reject/all/allSettled/race/any(...)`. This cannot see a
277
+ * promise returned by an opaque call (e.g. `fetchData()` whose return type is a
278
+ * promise), which a syntactic lint rule has no type information for.
279
+ */
280
+ function isPromiseReturningExpr(node) {
281
+ if (!node) return false;
282
+ if (node.type === "CallExpression" && node.callee?.type === "MemberExpression" && !node.callee.computed && node.callee.property?.type === "Identifier") {
283
+ const name = node.callee.property.name;
284
+ if (name === "then" || name === "catch" || name === "finally") return true;
285
+ if (node.callee.object?.type === "Identifier" && node.callee.object.name === "Promise") return true;
286
+ }
287
+ if (node.type === "NewExpression" && node.callee?.type === "Identifier" && node.callee.name === "Promise") return true;
288
+ return false;
289
+ }
290
+ /**
291
+ * Whether a block contains a `return <promise>` statement, without descending
292
+ * into nested functions (whose returns belong to them, not to the block's
293
+ * function).
294
+ */
295
+ function blockReturnsPromise(node, depth = 0) {
296
+ if (depth > MAX_RECURSION_DEPTH || !node || typeof node !== "object") return false;
297
+ if (node.type === "ReturnStatement") return isPromiseReturningExpr(node.argument);
298
+ if (node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression" || node.type === "FunctionDeclaration") return false;
299
+ for (const key in node) {
300
+ if (key === "type" || key === "start" || key === "end" || key === "loc" || key === "parent" || key === "range") continue;
301
+ const child = node[key];
302
+ if (Array.isArray(child)) {
303
+ for (const item of child) if (typeof item === "object" && item !== null && blockReturnsPromise(item, depth + 1)) return true;
304
+ } else if (typeof child === "object" && child !== null) {
305
+ if (blockReturnsPromise(child, depth + 1)) return true;
306
+ }
307
+ }
308
+ return false;
309
+ }
310
+ /**
311
+ * Whether `fn` is a function (arrow, function expression, or function
312
+ * declaration) whose body returns a syntactically promise-typed value, even
313
+ * though it is not declared `async`. LogTape awaits any promise the lazy
314
+ * callback returns, e.g. `() => fetchData().then((data) => ({ data }))`, so a
315
+ * non-async helper resolved by reference needs the same handling as an inline
316
+ * one.
317
+ */
318
+ function isPromiseReturningCallback(fn) {
319
+ if (fn?.type !== "ArrowFunctionExpression" && fn?.type !== "FunctionExpression" && fn?.type !== "FunctionDeclaration") return false;
320
+ const body = fn.body;
321
+ if (body && body.type !== "BlockStatement") return isPromiseReturningExpr(body);
322
+ return blockReturnsPromise(body);
323
+ }
324
+ /**
325
+ * Whether `arrayNode` is the argument array of a Promise combinator that awaits
326
+ * every element, i.e. `Promise.all([...])` or `Promise.allSettled([...])`.
327
+ * Those consume all element promises, so a log promise inside the array is
328
+ * still awaited when the combinator is awaited. `Promise.race`/`Promise.any`
329
+ * are excluded: they can settle on another promise before the log promise
330
+ * resolves, so the log write is not guaranteed to flush. A bare array literal,
331
+ * by contrast, does not preserve promise semantics at all.
332
+ */
333
+ function isPromiseCombinatorArrayArg(arrayNode) {
334
+ const parent = arrayNode.parent;
335
+ if (!parent || parent.type !== "CallExpression") return false;
336
+ if (!(parent.arguments?.includes(arrayNode) ?? false)) return false;
337
+ const callee = parent.callee;
338
+ return callee?.type === "MemberExpression" && !callee.computed && callee.object?.type === "Identifier" && callee.object.name === "Promise" && callee.property?.type === "Identifier" && ["all", "allSettled"].includes(callee.property.name);
339
+ }
340
+ /**
341
+ * Walk the ancestor chain of a log call to decide whether the promise it
342
+ * returns is awaited, returned, or otherwise propagated to a caller that can
343
+ * await it. Returns `true` when the promise is handled and the call therefore
344
+ * needs no `await`.
345
+ *
346
+ * Handled: the call is awaited, returned from a non-discarding function, the
347
+ * concise body of a non-discarding arrow, or chained with
348
+ * `.then`/`.catch`/`.finally`, or it sits inside `Promise.all`/`allSettled`.
349
+ * Not handled (the walk stops and returns `false`): the promise is wrapped in
350
+ * an object/array literal, awaiting a `map()`/`flatMap()` array (which awaits
351
+ * the array, not its element promises), dropped by a discarding callback
352
+ * (`forEach` etc.), or it reaches a statement boundary unconsumed.
353
+ */
354
+ function isLogPromiseHandled(node) {
355
+ let current = node;
356
+ while (current) {
357
+ const ancestor = current.parent;
358
+ if (!ancestor) break;
359
+ if (ancestor.type === "AwaitExpression") {
360
+ if (isMapResultChain(current)) break;
361
+ return true;
362
+ }
363
+ if (ancestor.type === "ReturnStatement") {
364
+ if (isMapResultChain(current)) break;
365
+ const fn = findEnclosingFunction(ancestor);
366
+ if (fn && isCallArgumentFunction(fn)) {
367
+ if (isDiscardedCallbackArgument(fn)) break;
368
+ current = fn;
369
+ continue;
370
+ }
371
+ return true;
372
+ }
373
+ if (ancestor.type === "ArrowFunctionExpression" && ancestor.body === current) {
374
+ if (!isCallArgumentFunction(ancestor)) return true;
375
+ if (isDiscardedCallbackArgument(ancestor)) break;
376
+ }
377
+ if (ancestor.type === "MemberExpression") {
378
+ const prop = ancestor.property;
379
+ const isPromiseMethod = !ancestor.computed ? prop?.type === "Identifier" && [
380
+ "then",
381
+ "catch",
382
+ "finally"
383
+ ].includes(prop.name) : prop?.type === "Literal" && [
384
+ "then",
385
+ "catch",
386
+ "finally"
387
+ ].includes(prop.value);
388
+ if (isPromiseMethod) {
389
+ const grandAncestor = ancestor.parent;
390
+ if (grandAncestor?.type === "CallExpression" && grandAncestor.callee === ancestor) return true;
391
+ }
392
+ }
393
+ if (ancestor.type === "ObjectExpression") break;
394
+ if (ancestor.type === "ArrayExpression") {
395
+ if (!isPromiseCombinatorArrayArg(ancestor) || isMapResultChain(current)) break;
396
+ }
397
+ if (ancestor.type === "SequenceExpression" && ancestor.expressions?.[ancestor.expressions.length - 1] !== current) break;
398
+ if (ancestor.type === "UnaryExpression" || ancestor.type === "BinaryExpression") break;
399
+ if (ancestor.type === "LogicalExpression" && ancestor.operator === "&&" && ancestor.left === current) break;
400
+ if (ancestor.type === "ConditionalExpression" && ancestor.test === current) break;
401
+ if (ancestor.type === "ExpressionStatement" || ancestor.type === "VariableDeclarator" || ancestor.type === "AssignmentExpression") break;
402
+ current = ancestor;
403
+ }
404
+ return false;
405
+ }
406
+ /**
407
+ * Whether `await ` can be safely inserted before a log call as an autofix.
408
+ * Only a standalone statement inside an async function qualifies: inserting
409
+ * `await` where the call's value is used (assigned, passed as an argument,
410
+ * returned) would change a `Promise<void>` into `void` and can break code that
411
+ * uses that promise.
412
+ */
413
+ function canInsertAwait(node) {
414
+ const enclosingFn = findEnclosingFunction(node);
415
+ return enclosingFn != null && enclosingFn.async === true && node.parent?.type === "ExpressionStatement";
416
+ }
417
+ /**
418
+ * Resolve the static name of an object property key, or `null` when the key is
419
+ * computed from a non-literal expression (e.g. `[someVar]`). A plain key
420
+ * (`loggers`), a string-literal key (`"loggers"`), and a computed
421
+ * string-literal key (`["loggers"]`) all resolve to their name, but a computed
422
+ * identifier key like `[loggers]` (a variable) does not.
423
+ */
424
+ function staticKeyName(prop) {
425
+ if (!prop.computed) {
426
+ if (typeof prop.key?.name === "string") return prop.key.name;
427
+ if (typeof prop.key?.value === "string") return prop.key.value;
428
+ return null;
429
+ }
430
+ if (prop.key?.type === "Literal" && typeof prop.key.value === "string") return prop.key.value;
431
+ if (prop.key?.type === "TemplateLiteral" && prop.key.expressions?.length === 0 && prop.key.quasis?.length === 1) {
432
+ const cooked = prop.key.quasis[0]?.value?.cooked ?? prop.key.quasis[0]?.cooked;
433
+ return typeof cooked === "string" ? cooked : null;
434
+ }
435
+ return null;
436
+ }
437
+ /**
438
+ * Whether an AST node is the string `value`, written either as a plain string
439
+ * literal or as a template literal with no interpolations (a backtick constant
440
+ * such as `` `logtape` ``). Deno's `TemplateElement` exposes `cooked`
441
+ * directly; ESTree nests it under `value.cooked`, so accept either shape.
442
+ */
443
+ function isStringLiteral(node, value) {
444
+ if (node?.type === "Literal") return node.value === value;
445
+ if (node?.type === "TemplateLiteral") {
446
+ const cooked = node.quasis?.[0]?.value?.cooked ?? node.quasis?.[0]?.cooked;
447
+ return node.expressions?.length === 0 && node.quasis?.length === 1 && cooked === value;
448
+ }
449
+ return false;
450
+ }
451
+ /**
452
+ * Whether an AST node is the array literal `["logtape"]` or
453
+ * `["logtape", "meta"]`.
454
+ */
455
+ function isLogtapeMetaArray(node) {
456
+ if (node?.type !== "ArrayExpression") return false;
457
+ const elems = node.elements;
458
+ if (!isStringLiteral(elems[0], "logtape")) return false;
459
+ if (elems.length === 1) return true;
460
+ return elems.length === 2 && isStringLiteral(elems[1], "meta");
461
+ }
462
+ /**
463
+ * Whether a logger entry covers the meta category with at least one non-empty
464
+ * sinks list. Only the array form (`["logtape"]` or `["logtape", "meta"]`)
465
+ * configures the meta logger: core's `configureInternal()` meta check inspects
466
+ * the category as an array, so a bare string `category: "logtape"` leaves the
467
+ * meta logger unconfigured and must not satisfy the rule.
468
+ */
469
+ function isMetaLoggerEntry(entry) {
470
+ if (!entry || entry.type !== "ObjectExpression") return false;
471
+ if (entry.properties?.some((p) => p.type === "SpreadElement")) return true;
472
+ let categoryNode = null;
473
+ let sinksNode = null;
474
+ for (const prop of entry.properties) {
475
+ if (prop.type !== "Property") continue;
476
+ const keyName = staticKeyName(prop);
477
+ if (keyName === "category") categoryNode = unwrapTypeAssertion(prop.value);
478
+ if (keyName === "sinks") sinksNode = unwrapTypeAssertion(prop.value);
479
+ }
480
+ if (!categoryNode) return false;
481
+ if (!isLogtapeMetaArray(categoryNode)) return false;
482
+ if (!sinksNode) return false;
483
+ if (sinksNode.type !== "ArrayExpression") return true;
484
+ return sinksNode.elements.length > 0;
485
+ }
486
+ /**
487
+ * Whether a `configure()`/`configureSync()` argument lacks a dedicated meta
488
+ * logger sink and so should be reported. Returns `false` (no report) when the
489
+ * argument is not an object literal, uses spread elements that may supply the
490
+ * meta logger, or already has a logger entry for the meta category.
491
+ */
492
+ function configNeedsMetaSink(configArg) {
493
+ if (!configArg || configArg.type !== "ObjectExpression") return false;
494
+ const properties = configArg.properties ?? [];
495
+ const loggersProperty = properties.find((p) => p.type === "Property" && staticKeyName(p) === "loggers");
496
+ const hasSpread = properties.some((p) => p.type === "SpreadElement");
497
+ if (!loggersProperty) return !hasSpread;
498
+ const loggersValue = unwrapTypeAssertion(loggersProperty.value);
499
+ if (loggersValue.type !== "ArrayExpression") return false;
500
+ const hasLoggerSpread = loggersValue.elements?.some((el) => el?.type === "SpreadElement");
501
+ if (hasLoggerSpread) return false;
502
+ return !loggersValue.elements?.some(isMetaLoggerEntry);
503
+ }
504
+
505
+ //#endregion
506
+ exports.LOG_METHODS = LOG_METHODS;
507
+ exports.canInsertAwait = canInsertAwait;
508
+ exports.configNeedsMetaSink = configNeedsMetaSink;
509
+ exports.containsAwaitOrYield = containsAwaitOrYield;
510
+ exports.isAsyncFunctionExpr = isAsyncFunctionExpr;
511
+ exports.isLogPromiseHandled = isLogPromiseHandled;
512
+ exports.isLogtapeImportSource = isLogtapeImportSource;
513
+ exports.isPromiseReturningCallback = isPromiseReturningCallback;
514
+ exports.logMethodName = logMethodName;
515
+ exports.propsHaveEagerCall = propsHaveEagerCall;
516
+ exports.selectLazyPropsObject = selectLazyPropsObject;
517
+ exports.unwrapTypeAssertion = unwrapTypeAssertion;