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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
10 changes: 10 additions & 0 deletions fixtures/any-usage/nested-weak-types.ts
Original file line number Diff line number Diff line change
@@ -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' };
7 changes: 7 additions & 0 deletions src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
23 changes: 23 additions & 0 deletions src/parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,29 @@ describe('classifyTypeAnnotation for nested weak types', () => {
const nodes = parseSource('const value: Promise<any> = 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', () => {
Expand Down
17 changes: 14 additions & 3 deletions src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading