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
22 changes: 21 additions & 1 deletion src/cli.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { describe, it, expect } from 'vitest';
import { execSync, spawnSync } from 'node:child_process';
import { readFileSync, rmSync, writeFileSync } from 'node:fs';
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

describe('CLI integration', () => {
const cli = 'npx tsx src/cli.ts';
Expand Down Expand Up @@ -36,6 +38,24 @@ describe('CLI integration', () => {
});
});

it('keeps JSON output parseable when saving a baseline', () => {
const directory = mkdtempSync(join(tmpdir(), 'typegap-baseline-'));
const baseline = join(directory, 'coverage.json');
try {
const result = spawnSync('npx', [
'tsx', 'src/cli.ts', 'fixtures/any-usage', '--format', 'json', '--baseline', baseline,
], { encoding: 'utf8' });

expect(result.status).toBe(0);
const report = JSON.parse(result.stdout) as { total: number; coverage: number };
const saved = JSON.parse(readFileSync(baseline, 'utf8')) as { total: number; coverage: number };
expect(saved).toMatchObject({ total: report.total, coverage: report.coverage });
expect(result.stderr).toContain(`Baseline saved to ${baseline}`);
} finally {
rmSync(directory, { recursive: true, force: true });
}
});

it('shows detail mode', () => {
const output = execSync(`${cli} fixtures/mixed --detail`, { encoding: 'utf-8' });
expect(output).toContain('Details');
Expand Down
4 changes: 3 additions & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,9 @@ program

if (opts.baseline) {
saveBaseline(result, resolve(opts.baseline as string), process.cwd());
console.log(`Baseline saved to ${opts.baseline}`);
const confirmation = `Baseline saved to ${opts.baseline}`;
if (format === 'json') console.error(confirmation);
else console.log(confirmation);
}

process.exit(exitCode);
Expand Down
10 changes: 10 additions & 0 deletions src/parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,16 @@ describe('classifyTypeAnnotation for TSInferType', () => {
});

describe('classifyTypeAnnotation for nested weak types', () => {
it.each([
['keyof any', AnnotationStatus.any],
['keyof unknown', AnnotationStatus.unknown],
['readonly any[]', AnnotationStatus.any],
['readonly unknown[]', AnnotationStatus.unknown],
])('classifies a type-operator wrapped %s', (annotation, expected) => {
const nodes = parseSource(`const value: ${annotation} = null as never;`);
expect(nodes.find(node => node.kind === 'var')?.status).toBe(expected);
});

it.each([
['property', '{ value: any }', AnnotationStatus.any],
['unknown property', '{ value: unknown }', AnnotationStatus.unknown],
Expand Down
10 changes: 9 additions & 1 deletion src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -426,13 +426,21 @@ export function classifyTypeAnnotation(typeNode: TSESTree.TypeNode): AnnotationS
return classifyTypeAnnotation(rt.typeAnnotation);
}

// TSTypeOperator
if (typeNode.type === 'TSTypeOperator') {
const operator = typeNode as TSESTree.TSTypeOperator;
return operator.typeAnnotation
? classifyTypeAnnotation(operator.typeAnnotation)
: AnnotationStatus.explicit;
}

// TSNamedTupleMember
if (typeNode.type === 'TSNamedTupleMember') {
const ntm = typeNode as TSESTree.TSNamedTupleMember;
return classifyTypeAnnotation(ntm.elementType);
}

// TSTypeOperator, TSInferType, TSIndexedAccessType handled above
// TSInferType and TSIndexedAccessType are handled above
// Everything else is explicit
return AnnotationStatus.explicit;
}
Expand Down
Loading