proteum 2.5.16 → 2.5.17
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/eslint.js +34 -5
- package/package.json +1 -1
- package/tests/error-boundary-rules.test.cjs +40 -0
package/eslint.js
CHANGED
|
@@ -183,6 +183,8 @@ const hasOptionalCallBoundary = (callExpression, ancestors = []) => {
|
|
|
183
183
|
return hasOptional || ancestors.some(({ node }) => node.type === 'ChainExpression');
|
|
184
184
|
};
|
|
185
185
|
|
|
186
|
+
// A loop body is iteration, not a condition. `for (const item of batch) item.reject(error)`
|
|
187
|
+
// preserves the error for every item there is, so it is not a swallow.
|
|
186
188
|
const isUnderConditionalControlFlow = (ancestors = []) =>
|
|
187
189
|
ancestors.some(({ node, childKey }) => {
|
|
188
190
|
if (node.type === 'IfStatement') return childKey === 'consequent' || childKey === 'alternate';
|
|
@@ -190,13 +192,33 @@ const isUnderConditionalControlFlow = (ancestors = []) =>
|
|
|
190
192
|
if (node.type === 'LogicalExpression') return childKey === 'right';
|
|
191
193
|
if (node.type === 'SwitchCase') return childKey === 'consequent';
|
|
192
194
|
|
|
193
|
-
return
|
|
194
|
-
['ForInStatement', 'ForOfStatement', 'ForStatement', 'WhileStatement', 'DoWhileStatement'].includes(
|
|
195
|
-
node.type,
|
|
196
|
-
) && childKey === 'body'
|
|
197
|
-
);
|
|
195
|
+
return false;
|
|
198
196
|
});
|
|
199
197
|
|
|
198
|
+
/**
|
|
199
|
+
* Does the handler throw, whatever it throws?
|
|
200
|
+
*
|
|
201
|
+
* `catch { throw new Error('must be an absolute URL') }` translates a failure
|
|
202
|
+
* into the domain's own vocabulary. The original object is dropped, but the
|
|
203
|
+
* failure still propagates and nothing continues silently, which is what this
|
|
204
|
+
* rule exists to prevent. Attaching the original as `cause` is better practice,
|
|
205
|
+
* not a separate correctness question for this rule.
|
|
206
|
+
*/
|
|
207
|
+
const handlerThrows = (node) => {
|
|
208
|
+
let throws = false;
|
|
209
|
+
|
|
210
|
+
traverseNode(node, (child, _parent, _parentKey, ancestors) => {
|
|
211
|
+
if (child.type === 'ThrowStatement' && !isUnderConditionalControlFlow(ancestors)) throws = true;
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
return throws;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
// A returned fallback is deliberately NOT accepted. `catch { return null }` is
|
|
218
|
+
// the textbook swallow, and no structural signal separates it from
|
|
219
|
+
// `.catch(() => [])`. Where a fallback really is correct, the call site says so
|
|
220
|
+
// with a disable comment and a reason, which stays greppable.
|
|
221
|
+
|
|
200
222
|
const defaultErrorReporters = ['app.reportError', 'app.handleError'];
|
|
201
223
|
|
|
202
224
|
/**
|
|
@@ -363,6 +385,11 @@ const createSwallowedErrorRule = () => ({
|
|
|
363
385
|
const reportHandler = (node, params, body) => {
|
|
364
386
|
const names = params.flatMap((param) => collectPatternNames(param));
|
|
365
387
|
if (names.length === 0) {
|
|
388
|
+
// Nothing was bound, so nothing can be routed. Still accepted when
|
|
389
|
+
// the handler translates the failure into a throw, because the
|
|
390
|
+
// failure keeps propagating rather than being continued past.
|
|
391
|
+
if (handlerThrows(body)) return;
|
|
392
|
+
|
|
366
393
|
context.report({ node, messageId: 'missingParam' });
|
|
367
394
|
return;
|
|
368
395
|
}
|
|
@@ -373,6 +400,8 @@ const createSwallowedErrorRule = () => ({
|
|
|
373
400
|
return;
|
|
374
401
|
}
|
|
375
402
|
|
|
403
|
+
if (handlerThrows(body)) return;
|
|
404
|
+
|
|
376
405
|
if (!handlerPreservesCaughtError(body, collectDerivedErrorNames(body, names), side, reporters)) {
|
|
377
406
|
context.report({ node, messageId: 'unpreserved', data: { name: referencedName } });
|
|
378
407
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "proteum",
|
|
3
3
|
"description": "LLM-first Opinionated Typescript Framework for web applications.",
|
|
4
|
-
"version": "2.5.
|
|
4
|
+
"version": "2.5.17",
|
|
5
5
|
"author": "Gaetan Le Gac (https://github.com/gaetanlegac)",
|
|
6
6
|
"repository": "git://github.com/gaetanlegac/proteum.git",
|
|
7
7
|
"license": "MIT",
|
|
@@ -240,6 +240,46 @@ test('an error pushed into a result collection counts as preservation', () => {
|
|
|
240
240
|
assert.equal(count(messages, swallowedRuleId), 0);
|
|
241
241
|
});
|
|
242
242
|
|
|
243
|
+
test('translating a failure into a new throw counts, even without the original', () => {
|
|
244
|
+
const messages = lint(`
|
|
245
|
+
export const assertUrl = (raw) => {
|
|
246
|
+
try {
|
|
247
|
+
return new URL(raw);
|
|
248
|
+
} catch {
|
|
249
|
+
throw new Error('must be an absolute URL');
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
`);
|
|
253
|
+
|
|
254
|
+
assert.equal(count(messages, swallowedRuleId), 0);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test('preservation inside a loop body counts, because iteration is not a condition', () => {
|
|
258
|
+
const messages = lint(`
|
|
259
|
+
export const run = (batch) => {
|
|
260
|
+
load().catch((error) => {
|
|
261
|
+
for (const item of batch) {
|
|
262
|
+
item.reject(error);
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
};
|
|
266
|
+
`);
|
|
267
|
+
|
|
268
|
+
assert.equal(count(messages, swallowedRuleId), 0);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test('a returned fallback is still a swallow, however it is spelled', () => {
|
|
272
|
+
const nullFallback = lint(`
|
|
273
|
+
export const run = () => {
|
|
274
|
+
try { risky(); } catch { return null; }
|
|
275
|
+
};
|
|
276
|
+
`);
|
|
277
|
+
assert.equal(count(nullFallback, swallowedRuleId), 1);
|
|
278
|
+
|
|
279
|
+
const emptyList = lint(`export const run = (d) => dns.resolveMx(d).catch(() => []);`);
|
|
280
|
+
assert.equal(count(emptyList, swallowedRuleId), 1);
|
|
281
|
+
});
|
|
282
|
+
|
|
243
283
|
test('a console-only catch is still reported', () => {
|
|
244
284
|
const messages = lint(`
|
|
245
285
|
export const run = async () => {
|