Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>`, `Record<string,
any>`), 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

Expand Down
46 changes: 46 additions & 0 deletions src/parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<unknown | any>', 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 };

Expand Down
58 changes: 49 additions & 9 deletions src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 */
/* ------------------------------------------------------------------ */
Expand Down Expand Up @@ -241,22 +288,15 @@ 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;
}

// 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
Expand Down
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading