diff --git a/readme.md b/readme.md index 9f13621a..55686df0 100644 --- a/readme.md +++ b/readme.md @@ -191,10 +191,13 @@ You can use the programmatic API to retrieve the diagnostics and do something wi import tsd from 'tsd'; (async () => { - const diagnostics = await tsd(); + const diagnoser = await tsd(); - console.log(diagnostics.length); - //=> 2 + // Returns the number of tests evaluated. + console.log(diagnoser.testCount) + + // Returns the diagnostics if any or just an empty array. + console.log(diagnoser.diagnostics); })(); ``` @@ -213,6 +216,19 @@ Default: `process.cwd()` Current working directory of the project to retrieve the diagnostics for. +##### typingsFile + +Type: `string`
+Default: The `types` property in `package.json`. + +Path to the type definition file you want to test. This can be useful when using a test runner to test specific type definitions per test. + +##### testFiles + +Type: `string[]`
+Default: Finds files with `.test-d.ts` or `.test-d.tsx` extension. + +An array of test files with their path. Uses [globby](https://github.com/sindresorhus/globby) under the hood so that you can fine tune test file discovery. ## License diff --git a/source/cli.ts b/source/cli.ts index 6712b892..e9d6cc6f 100644 --- a/source/cli.ts +++ b/source/cli.ts @@ -23,10 +23,11 @@ const cli = meow(` try { const options = cli.input.length > 0 ? {cwd: cli.input[0]} : undefined; - const diagnostics = await tsd(options); + const extendedDiagnostics = await tsd(options); + const diagnostics = extendedDiagnostics.diagnostics; if (diagnostics.length > 0) { - throw new Error(formatter(diagnostics)); + throw new Error(formatter(extendedDiagnostics)); } } catch (error) { console.error(error.message); diff --git a/source/lib/compiler.ts b/source/lib/compiler.ts index 07d67582..8fada40b 100644 --- a/source/lib/compiler.ts +++ b/source/lib/compiler.ts @@ -1,4 +1,3 @@ -import * as path from 'path'; import { flattenDiagnosticMessageText, createProgram, @@ -7,7 +6,7 @@ import { } from '../../libraries/typescript'; import {TypeChecker} from './entities/typescript'; import {extractAssertions, parseErrorAssertionToLocation} from './parser'; -import {Diagnostic, DiagnosticCode, Context, Location} from './interfaces'; +import {Diagnostic, DiagnosticCode, Context, Location, ExtendedDiagnostic} from './interfaces'; import {handle} from './assertions'; // List of diagnostic codes that should be ignored in general @@ -64,12 +63,11 @@ const ignoreDiagnostic = (diagnostic: TSDiagnostic, expectedErrors: Map { - const fileNames = context.testFiles.map(fileName => path.join(context.cwd, fileName)); - +export const getDiagnostics = (context: Context): ExtendedDiagnostic => { + let testCount = 0; const diagnostics: Diagnostic[] = []; - const program = createProgram(fileNames, context.config.compilerOptions); + const program = createProgram(context.testFiles, context.config.compilerOptions); const tsDiagnostics = program .getSemanticDiagnostics() @@ -77,6 +75,10 @@ export const getDiagnostics = (context: Context): Diagnostic[] => { const assertions = extractAssertions(program); + for (const assertion of assertions) { + testCount = testCount + assertion[1].size; + } + diagnostics.push(...handle(program.getTypeChecker() as TypeChecker, assertions)); const expectedErrors = parseErrorAssertionToLocation(assertions); @@ -105,5 +107,5 @@ export const getDiagnostics = (context: Context): Diagnostic[] => { }); } - return diagnostics; + return {testCount, diagnostics}; }; diff --git a/source/lib/formatter.ts b/source/lib/formatter.ts index 3fdb23eb..9bafb36f 100644 --- a/source/lib/formatter.ts +++ b/source/lib/formatter.ts @@ -1,5 +1,5 @@ import * as formatter from 'eslint-formatter-pretty'; -import {Diagnostic} from './interfaces'; +import {ExtendedDiagnostic} from './interfaces'; /** * Format the TypeScript diagnostics to a human readable output. @@ -7,7 +7,8 @@ import {Diagnostic} from './interfaces'; * @param diagnostics - List of TypeScript diagnostics. * @returns Beautiful diagnostics output */ -export default (diagnostics: Diagnostic[]) => { +export default (extendedDiagnostics: ExtendedDiagnostic) => { + const diagnostics = extendedDiagnostics.diagnostics; const fileMap = new Map(); for (const diagnostic of diagnostics) { diff --git a/source/lib/index.ts b/source/lib/index.ts index 4e56dbb2..cac66d90 100644 --- a/source/lib/index.ts +++ b/source/lib/index.ts @@ -9,11 +9,12 @@ import {Context, Config} from './interfaces'; export interface Options { cwd: string; + typingsFile?: string; + testFiles?: string[]; } const findTypingsFile = async (pkg: any, options: Options) => { - const typings = pkg.types || pkg.typings || 'index.d.ts'; - + const typings = options.typingsFile || pkg.types || pkg.typings || 'index.d.ts'; const typingsExist = await pathExists(path.join(options.cwd, typings)); if (!typingsExist) { @@ -23,13 +24,37 @@ const findTypingsFile = async (pkg: any, options: Options) => { return typings; }; -const findTestFiles = async (typingsFile: string, options: Options & {config: Config}) => { +const normalizeTypingsFilePath = (typingsFilePath: string, options: Options) => { + if (options.typingsFile) { + return path.basename(typingsFilePath); + } + + return typingsFilePath; +}; + +const findCustomTestFiles = async (testFilesPattern: readonly string[], cwd: string) => { + const testFiles = await globby(testFilesPattern, {cwd}); + + if (testFiles.length === 0) { + throw new Error('Could not find any test files. Create one and try again'); + } + + return testFiles.map(file => path.join(cwd, file)); +}; + +const findTestFiles = async (typingsFilePath: string, options: Options & {config: Config}) => { + if (options.testFiles?.length) { + return findCustomTestFiles(options.testFiles, options.cwd); + } + + // Return only the filename if the `typingsFile` option is used. + const typingsFile = normalizeTypingsFilePath(typingsFilePath, options); + const testFile = typingsFile.replace(/\.d\.ts$/, '.test-d.ts'); const tsxTestFile = typingsFile.replace(/\.d\.ts$/, '.test-d.tsx'); const testDir = options.config.directory; let testFiles = await globby([testFile, tsxTestFile], {cwd: options.cwd}); - const testDirExists = await pathExists(path.join(options.cwd, testDir)); if (testFiles.length === 0 && !testDirExists) { @@ -40,7 +65,7 @@ const findTestFiles = async (typingsFile: string, options: Options & {config: Co testFiles = await globby([`${testDir}/**/*.ts`, `${testDir}/**/*.tsx`], {cwd: options.cwd}); } - return testFiles; + return testFiles.map(fileName => path.join(options.cwd, fileName)); }; /** @@ -56,7 +81,6 @@ export default async (options: Options = {cwd: process.cwd()}) => { } const pkg = pkgResult.packageJson; - const config = loadConfig(pkg as any, options.cwd); // Look for a typings file, otherwise use `index.d.ts` in the root directory. If the file is not found, throw an error. @@ -72,11 +96,19 @@ export default async (options: Options = {cwd: process.cwd()}) => { pkg, typingsFile, testFiles, - config + typingsFile, + cwd: options.cwd }; - return [ - ...getCustomDiagnostics(context), - ...getTSDiagnostics(context) - ]; + const tsDiagnostics = getTSDiagnostics(context); + const customDiagnostics = getCustomDiagnostics(context); + const testCount = tsDiagnostics.testCount; + + return { + testCount, + diagnostics: [ + ...customDiagnostics, + ...tsDiagnostics.diagnostics + ] + }; }; diff --git a/source/lib/interfaces.ts b/source/lib/interfaces.ts index bb289339..74344d43 100644 --- a/source/lib/interfaces.ts +++ b/source/lib/interfaces.ts @@ -38,6 +38,11 @@ export interface Diagnostic { column?: number; } +export interface ExtendedDiagnostic { + testCount: number; + diagnostics: Diagnostic[]; +} + export interface Location { fileName: string; start: number; diff --git a/source/lib/rules/index.ts b/source/lib/rules/index.ts index a4229e0a..d4f71634 100644 --- a/source/lib/rules/index.ts +++ b/source/lib/rules/index.ts @@ -16,7 +16,7 @@ const rules = new Set([ * @param context - The context object. * @returns List of diagnostics */ -export default (context: Context) => { +export default (context: Context): Diagnostic[] => { const diagnostics: Diagnostic[] = []; for (const rule of rules) { diff --git a/source/test/fixtures/specify-test-files/index.d.ts b/source/test/fixtures/specify-test-files/index.d.ts new file mode 100644 index 00000000..0616ebaa --- /dev/null +++ b/source/test/fixtures/specify-test-files/index.d.ts @@ -0,0 +1,6 @@ +declare const one: { + (foo: string, bar: string): string; + (foo: number, bar: number): number; +}; + +export default one; diff --git a/source/test/fixtures/specify-test-files/index.js b/source/test/fixtures/specify-test-files/index.js new file mode 100644 index 00000000..f17717f5 --- /dev/null +++ b/source/test/fixtures/specify-test-files/index.js @@ -0,0 +1,3 @@ +module.exports.default = (foo, bar) => { + return foo + bar; +}; diff --git a/source/test/fixtures/specify-test-files/package.json b/source/test/fixtures/specify-test-files/package.json new file mode 100644 index 00000000..de6dc1db --- /dev/null +++ b/source/test/fixtures/specify-test-files/package.json @@ -0,0 +1,3 @@ +{ + "name": "foo" +} diff --git a/source/test/fixtures/specify-test-files/unknown.test.ts b/source/test/fixtures/specify-test-files/unknown.test.ts new file mode 100644 index 00000000..080ee4ca --- /dev/null +++ b/source/test/fixtures/specify-test-files/unknown.test.ts @@ -0,0 +1,5 @@ +import {expectType} from '../../..'; +import one from '.'; + +expectType(one('foo', 'bar')); +expectType(one(1, 2)); diff --git a/source/test/fixtures/typings-custom-dir/index.js b/source/test/fixtures/typings-custom-dir/index.js new file mode 100644 index 00000000..f17717f5 --- /dev/null +++ b/source/test/fixtures/typings-custom-dir/index.js @@ -0,0 +1,3 @@ +module.exports.default = (foo, bar) => { + return foo + bar; +}; diff --git a/source/test/fixtures/typings-custom-dir/index.test-d.ts b/source/test/fixtures/typings-custom-dir/index.test-d.ts new file mode 100644 index 00000000..a6fedd96 --- /dev/null +++ b/source/test/fixtures/typings-custom-dir/index.test-d.ts @@ -0,0 +1,5 @@ +import {expectType} from '../../..'; +import one from './utils'; + +expectType(one('foo', 'bar')); +expectType(one(1, 2)); diff --git a/source/test/fixtures/typings-custom-dir/package.json b/source/test/fixtures/typings-custom-dir/package.json new file mode 100644 index 00000000..de6dc1db --- /dev/null +++ b/source/test/fixtures/typings-custom-dir/package.json @@ -0,0 +1,3 @@ +{ + "name": "foo" +} diff --git a/source/test/fixtures/typings-custom-dir/utils/index.d.ts b/source/test/fixtures/typings-custom-dir/utils/index.d.ts new file mode 100644 index 00000000..0616ebaa --- /dev/null +++ b/source/test/fixtures/typings-custom-dir/utils/index.d.ts @@ -0,0 +1,6 @@ +declare const one: { + (foo: string, bar: string): string; + (foo: number, bar: number): number; +}; + +export default one; diff --git a/source/test/fixtures/utils.ts b/source/test/fixtures/utils.ts index cab9e6e7..d915f59a 100644 --- a/source/test/fixtures/utils.ts +++ b/source/test/fixtures/utils.ts @@ -1,5 +1,5 @@ import {ExecutionContext} from 'ava'; -import {Diagnostic} from '../../lib/interfaces'; +import {ExtendedDiagnostic} from '../../lib/interfaces'; type Expectation = [number, number, 'error' | 'warning', string, (string | RegExp)?]; @@ -7,10 +7,11 @@ type Expectation = [number, number, 'error' | 'warning', string, (string | RegEx * Verify a list of diagnostics. * * @param t - The AVA execution context. - * @param diagnostics - List of diagnostics to verify. + * @param extendedDiagnostics - Object containing testCount and list of diagnostics to verify * @param expectations - Expected diagnostics. */ -export const verify = (t: ExecutionContext, diagnostics: Diagnostic[], expectations: Expectation[]) => { +export const verify = (t: ExecutionContext, extendedDiagnostics: ExtendedDiagnostic, expectations: Expectation[]) => { + const diagnostics = extendedDiagnostics.diagnostics; t.true(diagnostics.length === expectations.length); for (const [index, diagnostic] of diagnostics.entries()) { diff --git a/source/test/test.ts b/source/test/test.ts index ebe99335..e4501150 100644 --- a/source/test/test.ts +++ b/source/test/test.ts @@ -220,3 +220,42 @@ test('strict types', async t => { verify(t, diagnostics, []); }); + +test('typings in custom directory', async t => { + const diagnostics = await tsd({ + cwd: path.join(__dirname, 'fixtures/typings-custom-dir'), + typingsFile: 'utils/index.d.ts' + }); + + verify(t, diagnostics, [ + [5, 19, 'error', 'Argument of type \'number\' is not assignable to parameter of type \'string\'.'] + ]); +}); + +test('specify test files manually', async t => { + const diagnostics = await tsd({ + cwd: path.join(__dirname, 'fixtures/specify-test-files'), + testFiles: [ + 'unknown.test.ts' + ] + }); + + verify(t, diagnostics, [ + [5, 19, 'error', 'Argument of type \'number\' is not assignable to parameter of type \'string\'.'] + ]); +}); + +test('fails if typings file is not found in the specified path', async t => { + const error = await t.throwsAsync(tsd({ + cwd: path.join(__dirname, 'fixtures/typings-custom-dir'), + typingsFile: 'unknown.d.ts' + })); + + t.is(error.message, 'The type definition `unknown.d.ts` does not exist. Create one and try again.'); +}); + +test('checking testCount', async t => { + const diagnostics = await tsd({cwd: path.join(__dirname, 'fixtures/failure')}); + + t.is(diagnostics.testCount, 2); +});