From ca166ea3b43035a66721f14dcd9f00d9ef06b7fc Mon Sep 17 00:00:00 2001 From: Roger Chappel Date: Tue, 8 Sep 2026 08:41:14 +1000 Subject: [PATCH 1/3] test: cover constructor type annotations --- src/parser.test.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/parser.test.ts b/src/parser.test.ts index 531c1aa..08e5622 100644 --- a/src/parser.test.ts +++ b/src/parser.test.ts @@ -172,6 +172,26 @@ describe('classifyTypeAnnotation for nested weak types', () => { expect(nodes.find(node => node.kind === 'var')?.status).toBe(expected); }); + it.each([ + ['any parameter', 'new (value: any) => Widget', AnnotationStatus.any], + ['unknown parameter', 'new (value: unknown) => Widget', AnnotationStatus.unknown], + ['any return', 'new (value: string) => any', AnnotationStatus.any], + ['unknown return', 'new (value: string) => unknown', AnnotationStatus.unknown], + ['fully typed constructor', 'new (value: string) => Widget', AnnotationStatus.explicit], + ])('classifies a constructor type with an %s', (_name, annotation, expected) => { + const nodes = parseSource(`const Factory: ${annotation} = null as never;`); + expect(nodes.find(node => node.kind === 'var')?.status).toBe(expected); + }); + + it.each([ + 'new (value: unknown) => any', + 'new (value: any) => unknown', + 'new (first: unknown, second: any) => Widget', + ])('gives any deterministic precedence within constructor type %s', annotation => { + const nodes = parseSource(`const Factory: ${annotation} = null as never;`); + expect(nodes.find(node => node.kind === 'var')?.status).toBe(AnnotationStatus.any); + }); + it.each([ '{ first: unknown; second: any }', '{ first: any; second: unknown }', From 0008a9b49e1e613b59990318e2a475750f8db0f9 Mon Sep 17 00:00:00 2001 From: Roger Chappel Date: Tue, 8 Sep 2026 08:41:38 +1000 Subject: [PATCH 2/3] fix: classify constructor type parameters --- src/parser.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/parser.ts b/src/parser.ts index 68741c9..d278e57 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -385,8 +385,7 @@ export function classifyTypeAnnotation(typeNode: TSESTree.TypeNode): AnnotationS // TSConstructorType if (typeNode.type === 'TSConstructorType') { const ct = typeNode as TSESTree.TSConstructorType; - if (ct.returnType) return classifyTypeAnnotation(ct.returnType.typeAnnotation); - return AnnotationStatus.explicit; + return classifyFunctionTypeParts(ct.params, ct.returnType); } // TSTypePredicate — check the asserted type (`value is T` / `asserts value is T`) From 0b018708db3c0b5ad2c0bf9e6b669a4ecbc9d550 Mon Sep 17 00:00:00 2001 From: Roger Chappel Date: Tue, 8 Sep 2026 08:42:01 +1000 Subject: [PATCH 3/3] docs: document constructor signature coverage --- CHANGELOG.md | 1 + README.md | 12 ++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c560b57..610cb76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased - Reject malformed `--min-coverage` values instead of accepting numeric prefixes. +- Classify weak types in constructor signature parameters as well as returns. All notable changes to this project will be documented in this file. diff --git a/README.md b/README.md index 3a86947..139e82b 100644 --- a/README.md +++ b/README.md @@ -75,9 +75,9 @@ any>`). Class fields with explicit annotations report one property finding, including public, private, protected, static, readonly, declare, and optional fields. Interface declarations and direct object type aliases report their properties, method parameters and returns, and index signatures individually. -Function type parameters and returns nested in another annotation contribute to -that enclosing finding. When one annotation contains both, `any` takes -precedence over `unknown`. +Function and constructor signature parameters and returns nested in another +annotation contribute to that enclosing finding. When one annotation contains +both, `any` takes precedence over `unknown`. ## Output @@ -170,9 +170,9 @@ 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. +annotations, including function and constructor signatures, type predicates, +and mapped-type key constraints, remapped keys, and values. When both occur in +one annotation, `any` takes precedence. ## Contributing