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.
@@ -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 = (ancestors) =>
411
- ancestors.some(({ node }) => {
412
- const returnType = node?.returnType?.typeAnnotation;
413
- return returnType?.type === 'TSTypePredicate';
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
- const isWithinCatchParameter = (ancestors) =>
417
- ancestors.some(({ node, childKey }) => node?.type === 'CatchClause' && childKey === 'param');
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
- let depth = 0;
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.parent?.type === 'TSTypeAnnotation' && node.parent.parent?.type === 'Identifier') {
467
- const owner = node.parent.parent.parent;
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.15",
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
  ----------------------------------*/