@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.
- package/LICENSE +20 -0
- package/dist/core/ast.cjs +517 -0
- package/dist/core/ast.js +506 -0
- package/dist/core/ast.js.map +1 -0
- package/dist/deno/plugin.cjs +332 -0
- package/dist/deno/plugin.d.cts +46 -0
- package/dist/deno/plugin.d.cts.map +1 -0
- package/dist/deno/plugin.d.ts +46 -0
- package/dist/deno/plugin.d.ts.map +1 -0
- package/dist/deno/plugin.js +333 -0
- package/dist/deno/plugin.js.map +1 -0
- package/dist/eslint/plugin.cjs +51 -0
- package/dist/eslint/plugin.d.cts +26 -0
- package/dist/eslint/plugin.d.cts.map +1 -0
- package/dist/eslint/plugin.d.ts +26 -0
- package/dist/eslint/plugin.d.ts.map +1 -0
- package/dist/eslint/plugin.js +44 -0
- package/dist/eslint/plugin.js.map +1 -0
- package/dist/mod.cjs +15 -0
- package/dist/mod.d.cts +6 -0
- package/dist/mod.d.ts +6 -0
- package/dist/mod.js +7 -0
- package/dist/rules/no-message-interpolation.cjs +52 -0
- package/dist/rules/no-message-interpolation.d.cts +23 -0
- package/dist/rules/no-message-interpolation.d.cts.map +1 -0
- package/dist/rules/no-message-interpolation.d.ts +23 -0
- package/dist/rules/no-message-interpolation.d.ts.map +1 -0
- package/dist/rules/no-message-interpolation.js +53 -0
- package/dist/rules/no-message-interpolation.js.map +1 -0
- package/dist/rules/no-unawaited-log.cjs +67 -0
- package/dist/rules/no-unawaited-log.d.cts +21 -0
- package/dist/rules/no-unawaited-log.d.cts.map +1 -0
- package/dist/rules/no-unawaited-log.d.ts +21 -0
- package/dist/rules/no-unawaited-log.d.ts.map +1 -0
- package/dist/rules/no-unawaited-log.js +68 -0
- package/dist/rules/no-unawaited-log.js.map +1 -0
- package/dist/rules/prefer-lazy-evaluation.cjs +59 -0
- package/dist/rules/prefer-lazy-evaluation.d.cts +22 -0
- package/dist/rules/prefer-lazy-evaluation.d.cts.map +1 -0
- package/dist/rules/prefer-lazy-evaluation.d.ts +22 -0
- package/dist/rules/prefer-lazy-evaluation.d.ts.map +1 -0
- package/dist/rules/prefer-lazy-evaluation.js +60 -0
- package/dist/rules/prefer-lazy-evaluation.js.map +1 -0
- package/dist/rules/require-meta-sink.cjs +75 -0
- package/dist/rules/require-meta-sink.d.cts +27 -0
- package/dist/rules/require-meta-sink.d.cts.map +1 -0
- package/dist/rules/require-meta-sink.d.ts +27 -0
- package/dist/rules/require-meta-sink.d.ts.map +1 -0
- package/dist/rules/require-meta-sink.js +76 -0
- package/dist/rules/require-meta-sink.js.map +1 -0
- package/dist/utils.cjs +82 -0
- package/dist/utils.js +82 -0
- package/dist/utils.js.map +1 -0
- 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;
|