proteum 2.5.15 → 2.5.16
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/agents/project/CODING_STYLE.md +2 -0
- package/eslint.js +57 -23
- package/package.json +1 -1
- package/tests/error-boundary-rules.test.cjs +41 -0
|
@@ -107,6 +107,8 @@ Two rules police the edges of the type system and the error path. Both allow the
|
|
|
107
107
|
- the input of a type guard, since narrowing an untrusted value is the guard's whole job;
|
|
108
108
|
- anything carrying a `@boundary` tag in its doc comment.
|
|
109
109
|
|
|
110
|
+
Test files are exempt from this rule entirely: a fixture built with `as unknown as X` is a test technique, not a trust boundary. The other rules still apply in tests.
|
|
111
|
+
|
|
110
112
|
Use `@boundary` when the value genuinely arrives from outside: a parsed response, a provider payload, a third-party shim. State where it comes from, so the codebase's trust boundaries stay greppable:
|
|
111
113
|
|
|
112
114
|
```typescript
|
package/eslint.js
CHANGED
|
@@ -407,14 +407,39 @@ const boundaryTagPattern = /(^|\s)@boundary(\s|$)/;
|
|
|
407
407
|
* value is DomainField` is the correct signature. Any narrower input type would
|
|
408
408
|
* defeat the guard it belongs to.
|
|
409
409
|
*/
|
|
410
|
-
const isWithinTypeGuardSignature = (
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
410
|
+
const isWithinTypeGuardSignature = (node) => {
|
|
411
|
+
let current = node.parent;
|
|
412
|
+
while (current) {
|
|
413
|
+
if (current.returnType?.typeAnnotation?.type === 'TSTypePredicate') return true;
|
|
414
|
+
current = current.parent;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return false;
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Is this `unknown` inside the *parameter* of a catch clause?
|
|
422
|
+
*
|
|
423
|
+
* Range containment rather than a parent-chain shape check, so a destructured or
|
|
424
|
+
* annotated binding qualifies too. The containment test matters: the catch BODY
|
|
425
|
+
* also has the clause as an ancestor, and `unknown` there is not the language's
|
|
426
|
+
* doing and still needs a reason.
|
|
427
|
+
*/
|
|
428
|
+
const isWithinCatchParameter = (node) => {
|
|
429
|
+
let current = node.parent;
|
|
430
|
+
while (current) {
|
|
431
|
+
if (current.type === 'CatchClause') {
|
|
432
|
+
return (
|
|
433
|
+
current.param != null &&
|
|
434
|
+
node.range[0] >= current.param.range[0] &&
|
|
435
|
+
node.range[1] <= current.param.range[1]
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
current = current.parent;
|
|
439
|
+
}
|
|
415
440
|
|
|
416
|
-
|
|
417
|
-
|
|
441
|
+
return false;
|
|
442
|
+
};
|
|
418
443
|
|
|
419
444
|
const createNoLooseUnknownRule = () => ({
|
|
420
445
|
meta: {
|
|
@@ -440,13 +465,14 @@ const createNoLooseUnknownRule = () => ({
|
|
|
440
465
|
const comments = sourceCode.getCommentsBefore?.(node) || [];
|
|
441
466
|
if (comments.some((comment) => boundaryTagPattern.test(comment.value))) return true;
|
|
442
467
|
|
|
468
|
+
// Walk to the enclosing declaration rather than a fixed few levels:
|
|
469
|
+
// a nested position such as `{ query(v?: readonly unknown[]): Promise<unknown> }`
|
|
470
|
+
// sits eight or more nodes below the parameter the tag documents.
|
|
443
471
|
let current = node.parent;
|
|
444
|
-
|
|
445
|
-
while (current && depth < 6) {
|
|
472
|
+
while (current && current.type !== 'Program') {
|
|
446
473
|
const ancestorComments = sourceCode.getCommentsBefore?.(current) || [];
|
|
447
474
|
if (ancestorComments.some((comment) => boundaryTagPattern.test(comment.value))) return true;
|
|
448
475
|
current = current.parent;
|
|
449
|
-
depth += 1;
|
|
450
476
|
}
|
|
451
477
|
|
|
452
478
|
return false;
|
|
@@ -454,21 +480,10 @@ const createNoLooseUnknownRule = () => ({
|
|
|
454
480
|
|
|
455
481
|
return {
|
|
456
482
|
TSUnknownKeyword(node) {
|
|
457
|
-
const ancestors = [];
|
|
458
|
-
let current = node.parent;
|
|
459
|
-
while (current) {
|
|
460
|
-
ancestors.push({ childKey: null, node: current });
|
|
461
|
-
current = current.parent;
|
|
462
|
-
}
|
|
463
|
-
|
|
464
483
|
// TypeScript itself types a catch binding as `unknown`, so banning
|
|
465
484
|
// it there bans the language's own contract.
|
|
466
|
-
if (node
|
|
467
|
-
|
|
468
|
-
if (owner?.type === 'CatchClause') return;
|
|
469
|
-
}
|
|
470
|
-
if (isWithinCatchParameter(ancestors)) return;
|
|
471
|
-
if (isWithinTypeGuardSignature(ancestors)) return;
|
|
485
|
+
if (isWithinCatchParameter(node)) return;
|
|
486
|
+
if (isWithinTypeGuardSignature(node)) return;
|
|
472
487
|
if (hasBoundaryTag(node)) return;
|
|
473
488
|
|
|
474
489
|
context.report({ node, messageId: 'looseUnknown' });
|
|
@@ -859,12 +874,20 @@ const createValidDocAnchorRule = () => ({
|
|
|
859
874
|
},
|
|
860
875
|
});
|
|
861
876
|
|
|
877
|
+
const defaultTestFilePatterns = [
|
|
878
|
+
'**/*.test.{ts,tsx,mts,cts}',
|
|
879
|
+
'**/*.spec.{ts,tsx,mts,cts}',
|
|
880
|
+
'**/*.node-test.{ts,tsx,mts,cts}',
|
|
881
|
+
'**/tests/**/*.{ts,tsx,mts,cts}',
|
|
882
|
+
];
|
|
883
|
+
|
|
862
884
|
const createProteumEslintConfig = ({
|
|
863
885
|
docAnchors = 'warn',
|
|
864
886
|
errorReporters = defaultErrorReporters,
|
|
865
887
|
excludeDocAnchors = [],
|
|
866
888
|
includeDocAnchors = [],
|
|
867
889
|
ignores = [],
|
|
890
|
+
testFiles = defaultTestFilePatterns,
|
|
868
891
|
} = {}) => [
|
|
869
892
|
{
|
|
870
893
|
ignores: [...defaultIgnores, ...ignores],
|
|
@@ -936,8 +959,19 @@ const createProteumEslintConfig = ({
|
|
|
936
959
|
],
|
|
937
960
|
},
|
|
938
961
|
},
|
|
962
|
+
{
|
|
963
|
+
// A test fixture deliberately builds a shape the production types forbid,
|
|
964
|
+
// usually through `as unknown as X` or a partial mock. That is a test
|
|
965
|
+
// technique, not a trust boundary, so demanding a `@boundary` reason for
|
|
966
|
+
// each one would add noise without documenting anything real.
|
|
967
|
+
files: testFiles,
|
|
968
|
+
rules: {
|
|
969
|
+
'proteum/no-loose-unknown': 'off',
|
|
970
|
+
},
|
|
971
|
+
},
|
|
939
972
|
];
|
|
940
973
|
|
|
941
974
|
module.exports = {
|
|
942
975
|
createProteumEslintConfig,
|
|
976
|
+
defaultTestFilePatterns,
|
|
943
977
|
};
|
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.16",
|
|
5
5
|
"author": "Gaetan Le Gac (https://github.com/gaetanlegac)",
|
|
6
6
|
"repository": "git://github.com/gaetanlegac/proteum.git",
|
|
7
7
|
"license": "MIT",
|
|
@@ -36,6 +36,21 @@ test('unknown is allowed on a catch binding, which is how TypeScript types it',
|
|
|
36
36
|
assert.equal(count(messages, looseUnknownRuleId), 0);
|
|
37
37
|
});
|
|
38
38
|
|
|
39
|
+
test('unknown inside a catch BODY still needs a reason, only the binding is exempt', () => {
|
|
40
|
+
const messages = lint(`
|
|
41
|
+
export const run = () => {
|
|
42
|
+
try {
|
|
43
|
+
risky();
|
|
44
|
+
} catch (error: unknown) {
|
|
45
|
+
const detail: unknown = extract(error);
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
`);
|
|
50
|
+
|
|
51
|
+
assert.equal(count(messages, looseUnknownRuleId), 1);
|
|
52
|
+
});
|
|
53
|
+
|
|
39
54
|
test('unknown is allowed as the input of a type guard', () => {
|
|
40
55
|
const messages = lint(`
|
|
41
56
|
export const isDomainField = (value: unknown): value is string => typeof value === 'string';
|
|
@@ -59,6 +74,32 @@ test('an undocumented parse boundary is still reported', () => {
|
|
|
59
74
|
assert.equal(count(messages, looseUnknownRuleId), 1);
|
|
60
75
|
});
|
|
61
76
|
|
|
77
|
+
test('test files may use unknown without a boundary reason', () => {
|
|
78
|
+
const fixture = `const STATUS_RULE = [{ filterId: 'status' }] as unknown as RadarRulesContract;`;
|
|
79
|
+
|
|
80
|
+
assert.equal(count(lint(fixture, 'server/example.ts'), looseUnknownRuleId), 1);
|
|
81
|
+
assert.equal(count(lint(fixture, 'server/example.test.ts'), looseUnknownRuleId), 0);
|
|
82
|
+
assert.equal(count(lint(fixture, 'src/Domains/PendingList.node-test.ts'), looseUnknownRuleId), 0);
|
|
83
|
+
assert.equal(count(lint(fixture, 'tests/unit/scope-builder.ts'), looseUnknownRuleId), 0);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('the other rules still apply inside test files', () => {
|
|
87
|
+
const messages = lint(
|
|
88
|
+
`
|
|
89
|
+
export const run = async () => {
|
|
90
|
+
try {
|
|
91
|
+
await load();
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.error('load failed', error);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
`,
|
|
97
|
+
'server/example.test.ts',
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
assert.equal(count(messages, swallowedRuleId), 1);
|
|
101
|
+
});
|
|
102
|
+
|
|
62
103
|
/*----------------------------------
|
|
63
104
|
- no-swallowed-caught-error
|
|
64
105
|
----------------------------------*/
|