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
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@ 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>`). Interface declarations and direct object type aliases report their
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
Expand Down Expand Up @@ -148,7 +150,7 @@ CI should also run from a source checkout until the package is published:

1. Finds all `.ts` / `.tsx` files using glob
2. Parses each file with `@typescript-eslint/typescript-estree`
3. Walks the AST for functions, arrows, parameters, variables
3. Walks the AST for functions, arrows, parameters, variables, and class fields
4. Classifies each annotatable node: explicit, `any`, `unknown`, or implicit
5. Calculates coverage as `(total - implicit) / total * 100`

Expand All @@ -157,9 +159,12 @@ CI should also run from a source checkout until the package is published:
- Function / arrow return types
- Function / arrow parameters (including destructured and rest params)
- Variable declarations with explicit type annotations
- Class fields with explicit type annotations (including private and modified fields)
- Catch clause parameters

Constructor methods are excluded (they cannot have return types). Inferred-only variables (e.g., `const x = 5`) are not flagged — only explicit annotation sites are audited.
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.

## Contributing

Expand Down
8 changes: 8 additions & 0 deletions fixtures/any-usage/class-fields.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export class Service {
payload: any;
readonly response?: Promise<unknown>;

run(input: string): any {
return input;
}
}
8 changes: 8 additions & 0 deletions src/analyzer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,14 @@ describe('analyzeDirectory', () => {
const result = await analyzeDirectory('fixtures/any-usage');
expect(result.files.length).toBeGreaterThan(0);
expect(result.anyCount).toBeGreaterThan(0);
expect(result.files.find(file => file.file.endsWith('class-fields.ts'))).toMatchObject({
total: 4,
annotated: 4,
anyCount: 2,
unknownCount: 1,
implicitCount: 0,
coverage: 100,
});
});

it('analyzes missing-types fixture with implicit issues', async () => {
Expand Down
7 changes: 7 additions & 0 deletions src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,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('class-fields.ts'))).toMatchObject({
total: 4,
annotated: 4,
anyCount: 2,
unknownCount: 1,
coverage: 100,
});
});

it('shows detail mode', () => {
Expand Down
31 changes: 31 additions & 0 deletions src/parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,37 @@ describe('object member annotations', () => {
});
});

describe('class field annotations', () => {
it('reports supported class field forms without double-counting method bodies', () => {
const nodes = parseSource(`class Service {
payload: any;
public response?: Promise<unknown>;
private cache: Map<string, any> = new Map();
protected readonly label: string = 'service';
static count: number = 0;
declare config: { value: unknown };
#token: string = 'token';
run(input: unknown): any { return input; }
}`);

expect(nodes.map(({ kind, name, status }) => ({ kind, name, status }))).toEqual([
{ kind: 'property', name: 'payload', status: AnnotationStatus.any },
{ kind: 'property', name: 'response', status: AnnotationStatus.unknown },
{ kind: 'property', name: 'cache', status: AnnotationStatus.any },
{ kind: 'property', name: 'label', status: AnnotationStatus.explicit },
{ kind: 'property', name: 'count', status: AnnotationStatus.explicit },
{ kind: 'property', name: 'config', status: AnnotationStatus.unknown },
{ kind: 'property', name: '#token', status: AnnotationStatus.explicit },
{ kind: 'return', name: '(anonymous)() return', status: AnnotationStatus.any },
{ kind: 'param', name: 'input', status: AnnotationStatus.unknown },
]);
});

it('ignores inferred-only class fields consistently with variables', () => {
expect(parseSource('class Service { payload = 1; optional?; }')).toEqual([]);
});
});

describe('extractTypeName for edge cases', () => {
const opts = { loc: true, range: true, jsx: false, tokens: false, comment: false };

Expand Down
25 changes: 25 additions & 0 deletions src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ function visit(node: TSESTree.Node, file: string, nodes: NodeInfo[], parent: TSE
handleCatchClause(node as TSESTree.CatchClause, file, nodes);
break;

case 'PropertyDefinition':
handleClassField(node as TSESTree.PropertyDefinition, file, nodes);
break;

case 'TSInterfaceDeclaration':
handleObjectMembers((node as TSESTree.TSInterfaceDeclaration).body.body, file, nodes);
break;
Expand Down Expand Up @@ -195,6 +199,22 @@ function handleVariableDeclaration(
}
}

function handleClassField(
node: TSESTree.PropertyDefinition,
file: string,
nodes: NodeInfo[],
): void {
if (!node.typeAnnotation) return;

nodes.push(makeNode(
file,
node.loc?.start.line ?? 0,
'property',
classFieldName(node.key),
classifyTypeAnnotation(node.typeAnnotation.typeAnnotation),
));
}

function handleObjectMembers(
members: TSESTree.TypeElement[],
file: string,
Expand Down Expand Up @@ -232,6 +252,11 @@ function memberName(key: TSESTree.PropertyName): string {
return '[computed]';
}

function classFieldName(key: TSESTree.PropertyDefinition['key']): string {
if (key.type === 'PrivateIdentifier') return `#${key.name}`;
return memberName(key);
}

/* ------------------------------------------------------------------ */
/* Helpers */
/* ------------------------------------------------------------------ */
Expand Down
Loading