From 50518b5eea3db508c5c9f22b6bb81fc6d4c6bbed Mon Sep 17 00:00:00 2001 From: Roger Chappel Date: Mon, 7 Sep 2026 00:24:27 +1000 Subject: [PATCH 1/3] test: cover nested weak predicate and mapped types --- fixtures/any-usage/nested-weak-types.ts | 10 ++++++++++ src/cli.test.ts | 7 +++++++ src/parser.test.ts | 23 +++++++++++++++++++++++ 3 files changed, 40 insertions(+) create mode 100644 fixtures/any-usage/nested-weak-types.ts diff --git a/fixtures/any-usage/nested-weak-types.ts b/fixtures/any-usage/nested-weak-types.ts new file mode 100644 index 0000000..7f479df --- /dev/null +++ b/fixtures/any-usage/nested-weak-types.ts @@ -0,0 +1,10 @@ +export function isPayload(value: object): value is any { + return true; +} + +export function isUnknown(value: object): value is unknown { + return true; +} + +export const mapped: { [K in keyof any]: string } = {}; +export const cleanMapped: { [K in 'id' | 'name']: string } = { id: '1', name: 'example' }; diff --git a/src/cli.test.ts b/src/cli.test.ts index 0d1869e..4afa9b7 100644 --- a/src/cli.test.ts +++ b/src/cli.test.ts @@ -29,6 +29,13 @@ describe('CLI integration', () => { const json = JSON.parse(output); expect(json.coverage).toBe(100); // any counts as annotated (has a type) expect(json.anyCount).toBeGreaterThan(0); + expect(json.files.find((file: { file: string }) => file.file.endsWith('nested-weak-types.ts'))).toMatchObject({ + total: 6, + annotated: 6, + anyCount: 2, + unknownCount: 1, + coverage: 100, + }); expect(json.files.find((file: { file: string }) => file.file.endsWith('class-fields.ts'))).toMatchObject({ total: 4, annotated: 4, diff --git a/src/parser.test.ts b/src/parser.test.ts index 55e82dd..531c1aa 100644 --- a/src/parser.test.ts +++ b/src/parser.test.ts @@ -186,6 +186,29 @@ describe('classifyTypeAnnotation for nested weak types', () => { const nodes = parseSource('const value: Promise = Promise.resolve(1);'); expect(nodes.find(node => node.kind === 'var')?.status).toBe(AnnotationStatus.any); }); + + it.each([ + ['value is any', AnnotationStatus.any], + ['value is unknown', AnnotationStatus.unknown], + ['asserts value is any', AnnotationStatus.any], + ['value is string', AnnotationStatus.explicit], + ['asserts value', AnnotationStatus.explicit], + ])('classifies a type predicate containing %s', (predicate, expected) => { + const nodes = parseSource(`function check(value: object): ${predicate} { return true; }`); + expect(nodes.find(node => node.kind === 'return')?.status).toBe(expected); + }); + + it.each([ + ['constraint', '{ [K in keyof any]: string }', AnnotationStatus.any], + ['unknown constraint', '{ [K in keyof unknown]: string }', AnnotationStatus.unknown], + ['remapped key', '{ [K in string as any]: number }', AnnotationStatus.any], + ['value', '{ [K in string]: unknown }', AnnotationStatus.unknown], + ['any precedence', '{ [K in keyof unknown]: any }', AnnotationStatus.any], + ['fully typed mapped type', '{ [K in "name" | "id"]: string }', AnnotationStatus.explicit], + ])('classifies a mapped type %s', (_part, annotation, expected) => { + const nodes = parseSource(`const mapped: ${annotation} = {} as never;`); + expect(nodes.find(node => node.kind === 'var')?.status).toBe(expected); + }); }); describe('object member annotations', () => { From d25f49b22575c9aa69542057ee5a7a4c33e07e19 Mon Sep 17 00:00:00 2001 From: Roger Chappel Date: Mon, 7 Sep 2026 00:24:27 +1000 Subject: [PATCH 2/3] fix: traverse predicate and mapped type components --- src/parser.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/parser.ts b/src/parser.ts index 9935ebd..68741c9 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -389,11 +389,22 @@ export function classifyTypeAnnotation(typeNode: TSESTree.TypeNode): AnnotationS return AnnotationStatus.explicit; } - // TSMappedType + // TSTypePredicate — check the asserted type (`value is T` / `asserts value is T`) + if (typeNode.type === 'TSTypePredicate') { + const predicate = typeNode as TSESTree.TSTypePredicate; + return predicate.typeAnnotation + ? classifyTypeAnnotation(predicate.typeAnnotation.typeAnnotation) + : AnnotationStatus.explicit; + } + + // TSMappedType — inspect the key constraint, remapped key, and value if (typeNode.type === 'TSMappedType') { const mt = typeNode as TSESTree.TSMappedType; - if (mt.typeAnnotation) return classifyTypeAnnotation(mt.typeAnnotation); - return AnnotationStatus.explicit; + return combineWeakStatuses([ + classifyTypeAnnotation(mt.constraint), + mt.nameType ? classifyTypeAnnotation(mt.nameType) : AnnotationStatus.explicit, + mt.typeAnnotation ? classifyTypeAnnotation(mt.typeAnnotation) : AnnotationStatus.explicit, + ]); } // TSIndexedAccessType From 6bf4a2d355f5d47e0728587e2dfa9c7fe248f504 Mon Sep 17 00:00:00 2001 From: Roger Chappel Date: Mon, 7 Sep 2026 00:24:27 +1000 Subject: [PATCH 3/3] docs: define nested weak type propagation --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 66c1f5b..3a86947 100644 --- a/README.md +++ b/README.md @@ -169,6 +169,11 @@ Constructor methods are excluded (they cannot have return types). Inferred-only variables and class fields (for example, `const x = 5` or `value = 5`) are not flagged — only explicit annotation sites are audited. +Nested `any` and `unknown` are propagated through supported TypeScript type +annotations, including type predicates and mapped-type key constraints, +remapped keys, and values. When both occur in one annotation, `any` takes +precedence. + ## Contributing See [CONTRIBUTING.md](CONTRIBUTING.md) for setup and development guidelines.