From 5bddef4cc12cc7c21acf41465336a93883c98deb Mon Sep 17 00:00:00 2001 From: Roger Chappel Date: Tue, 18 Aug 2026 14:38:56 +1000 Subject: [PATCH 1/3] test: cover object member annotations --- src/parser.test.ts | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/parser.test.ts b/src/parser.test.ts index 631a839..8073b10 100644 --- a/src/parser.test.ts +++ b/src/parser.test.ts @@ -178,6 +178,52 @@ describe('classifyTypeAnnotation for nested weak types', () => { }); }); +describe('object member annotations', () => { + it('reports interface properties, methods, and index signatures', () => { + const nodes = parseSource(`interface Store { + value: unknown; + parse(input: any): unknown; + [key: string]: any; +}`); + + expect(nodes.map(({ kind, name, status }) => ({ kind, name, status }))).toEqual([ + { kind: 'property', name: 'value', status: AnnotationStatus.unknown }, + { kind: 'return', name: 'parse() return', status: AnnotationStatus.unknown }, + { kind: 'param', name: 'input', status: AnnotationStatus.any }, + { kind: 'return', name: '[index] value', status: AnnotationStatus.any }, + { kind: 'param', name: 'key', status: AnnotationStatus.explicit }, + ]); + }); + + it('reports members of type literals without changing variable findings', () => { + const aliasNodes = parseSource(`type Store = { + value: any; + parse(input: unknown): any; + [key: any]: unknown; +};`); + expect(aliasNodes.map(node => node.status)).toEqual([ + AnnotationStatus.any, + AnnotationStatus.any, + AnnotationStatus.unknown, + AnnotationStatus.unknown, + AnnotationStatus.any, + ]); + + const variableNodes = parseSource('const value: { item: any } = { item: 1 };'); + expect(variableNodes).toHaveLength(1); + expect(variableNodes[0]).toMatchObject({ kind: 'var', name: 'value', status: AnnotationStatus.any }); + }); + + it.each([ + ['unknown | any', AnnotationStatus.any], + ['any | unknown', AnnotationStatus.any], + ['Promise', AnnotationStatus.any], + ])('gives any precedence for a property annotation containing %s', (annotation, status) => { + const [node] = parseSource(`interface Value { item: ${annotation} }`); + expect(node).toMatchObject({ kind: 'property', name: 'item', status }); + }); +}); + describe('extractTypeName for edge cases', () => { const opts = { loc: true, range: true, jsx: false, tokens: false, comment: false }; From 5734aad81c11ed1970b61f925c19240910306141 Mon Sep 17 00:00:00 2001 From: Roger Chappel Date: Tue, 18 Aug 2026 14:39:26 +1000 Subject: [PATCH 2/3] feat: report interface and type alias members --- src/parser.ts | 58 +++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 49 insertions(+), 9 deletions(-) diff --git a/src/parser.ts b/src/parser.ts index c139fbf..06c700a 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -65,6 +65,16 @@ function visit(node: TSESTree.Node, file: string, nodes: NodeInfo[], parent: TSE case 'CatchClause': handleCatchClause(node as TSESTree.CatchClause, file, nodes); break; + + case 'TSInterfaceDeclaration': + handleObjectMembers((node as TSESTree.TSInterfaceDeclaration).body.body, file, nodes); + break; + + case 'TSTypeAliasDeclaration': { + const annotation = (node as TSESTree.TSTypeAliasDeclaration).typeAnnotation; + if (annotation.type === 'TSTypeLiteral') handleObjectMembers(annotation.members, file, nodes); + break; + } } // Recurse into children @@ -185,6 +195,43 @@ function handleVariableDeclaration( } } +function handleObjectMembers( + members: TSESTree.TypeElement[], + file: string, + nodes: NodeInfo[], +): void { + for (const member of members) { + if (member.type === 'TSPropertySignature' && member.typeAnnotation) { + nodes.push(makeNode( + file, + member.loc?.start.line ?? 0, + 'property', + memberName(member.key), + classifyTypeAnnotation(member.typeAnnotation.typeAnnotation), + )); + } else if (member.type === 'TSMethodSignature') { + const name = memberName(member.key); + const status = member.returnType + ? classifyTypeAnnotation(member.returnType.typeAnnotation) + : AnnotationStatus.implicit; + nodes.push(makeNode(file, member.loc?.start.line ?? 0, 'return', `${name}() return`, status)); + handleParams(member.params, file, nodes); + } else if (member.type === 'TSIndexSignature') { + const status = member.typeAnnotation + ? classifyTypeAnnotation(member.typeAnnotation.typeAnnotation) + : AnnotationStatus.implicit; + nodes.push(makeNode(file, member.loc?.start.line ?? 0, 'return', '[index] value', status)); + handleParams(member.parameters, file, nodes); + } + } +} + +function memberName(key: TSESTree.PropertyName): string { + if (key.type === 'Identifier') return key.name; + if (key.type === 'Literal') return String(key.value); + return '[computed]'; +} + /* ------------------------------------------------------------------ */ /* Helpers */ /* ------------------------------------------------------------------ */ @@ -241,10 +288,7 @@ export function classifyTypeAnnotation(typeNode: TSESTree.TypeNode): AnnotationS if (typeNode.type === 'TSTypeReference') { const ref = typeNode as TSESTree.TSTypeReference; if (ref.typeArguments) { - for (const p of ref.typeArguments.params) { - const childStatus = classifyTypeAnnotation(p); - if (childStatus !== AnnotationStatus.explicit) return childStatus; - } + return combineWeakStatuses(ref.typeArguments.params.map(classifyTypeAnnotation)); } return AnnotationStatus.explicit; } @@ -252,11 +296,7 @@ export function classifyTypeAnnotation(typeNode: TSESTree.TypeNode): AnnotationS // TSUnionType / TSIntersectionType if (typeNode.type === 'TSUnionType' || typeNode.type === 'TSIntersectionType') { const types = (typeNode as TSESTree.TSUnionType | TSESTree.TSIntersectionType).types; - for (const t of types) { - const s = classifyTypeAnnotation(t); - if (s !== AnnotationStatus.explicit) return s; - } - return AnnotationStatus.explicit; + return combineWeakStatuses(types.map(classifyTypeAnnotation)); } // TSArrayType → check elementType From bf99bc3ad37b2cc17372e02db070d1ec545ff065 Mon Sep 17 00:00:00 2001 From: Roger Chappel Date: Tue, 18 Aug 2026 14:39:41 +1000 Subject: [PATCH 3/3] docs: define object member finding scope --- README.md | 8 +++++--- src/types.ts | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e0d8e3a..3f3bfa2 100644 --- a/README.md +++ b/README.md @@ -71,9 +71,11 @@ console.log(result.coverage); | ⚠️ **implicit** | Missing type annotations — implicit `any` or missing return types | It also catches weak types nested in generics (`Array`, `Record`), object type properties, methods and index signatures, and function type -parameters or returns. When one annotation contains both, `any` takes precedence -over `unknown`. +any>`). 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`. ## Output diff --git a/src/types.ts b/src/types.ts index d5e82aa..813ac89 100644 --- a/src/types.ts +++ b/src/types.ts @@ -30,7 +30,7 @@ export interface NodeInfo { file: string; /** 1-based line number */ line: number; - /** Node kind hint (param, return, var, binding-param, generic-arg) */ + /** Node kind hint (param, return, var, property, binding-param, generic-arg) */ kind: string; /** The name or description of the node */ name: string;