From bb377a35663ab353b358eb6d2ad391f66bcd3ae7 Mon Sep 17 00:00:00 2001 From: Sam Verschueren Date: Tue, 12 Nov 2019 17:34:07 +0100 Subject: [PATCH] Add expectAssignable expectation - fixes #39 --- readme.md | 20 +++++++++- source/lib/assertions/assert.ts | 22 ++++++++++- .../lib/assertions/handlers/assignability.ts | 39 +++++++++++++++++++ source/lib/assertions/handlers/index.ts | 1 + source/lib/assertions/index.ts | 8 +++- source/test/assignability.ts | 21 ++++++++++ .../assignability/assignable/index.d.ts | 6 +++ .../assignability/assignable/index.js | 3 ++ .../assignability/assignable/index.test-d.ts | 8 ++++ .../assignability/assignable/package.json | 3 ++ .../assignability/not-assignable/index.d.ts | 6 +++ .../assignability/not-assignable/index.js | 3 ++ .../not-assignable/index.test-d.ts | 8 ++++ .../assignability/not-assignable/package.json | 3 ++ source/test/fixtures/utils.ts | 30 ++++++++++++++ source/test/test.ts | 32 +-------------- 16 files changed, 179 insertions(+), 34 deletions(-) create mode 100644 source/lib/assertions/handlers/assignability.ts create mode 100644 source/test/assignability.ts create mode 100644 source/test/fixtures/assignability/assignable/index.d.ts create mode 100644 source/test/fixtures/assignability/assignable/index.js create mode 100644 source/test/fixtures/assignability/assignable/index.test-d.ts create mode 100644 source/test/fixtures/assignability/assignable/package.json create mode 100644 source/test/fixtures/assignability/not-assignable/index.d.ts create mode 100644 source/test/fixtures/assignability/not-assignable/index.js create mode 100644 source/test/fixtures/assignability/not-assignable/index.test-d.ts create mode 100644 source/test/fixtures/assignability/not-assignable/package.json create mode 100644 source/test/fixtures/utils.ts diff --git a/readme.md b/readme.md index 431e049e..aaf0aa3e 100644 --- a/readme.md +++ b/readme.md @@ -77,6 +77,16 @@ If we run `tsd`, we will notice that it reports an error because the `concat` me +If you still want loose type assertion, you can use `expectAssignable` for that. + +```ts +import {expectType, expectAssignable} from 'tsd'; +import concat from '.'; + +expectType(concat('foo', 'bar')); +expectAssignable(concat('foo', 'bar')); +``` + ### Top-level `await` If your method returns a `Promise`, you can use top-level `await` to resolve the value instead of wrapping it in an `async` [IIFE](https://developer.mozilla.org/en-US/docs/Glossary/IIFE). @@ -142,7 +152,15 @@ These options will be overridden if a `tsconfig.json` file is found in your proj ### expectType<T>(value) -Check that `value` is identical to type `T`. +Check that the type of `value` is identical to type `T`. + +### expectAssignable<T>(value) + +Check that the type of `value` is assignable to type `T`. + +### expectNotAssignable<T>(value) + +Check that the type of `value` is not assignable to type `T`. ### expectError(function) diff --git a/source/lib/assertions/assert.ts b/source/lib/assertions/assert.ts index 522d3dd9..95442fdb 100644 --- a/source/lib/assertions/assert.ts +++ b/source/lib/assertions/assert.ts @@ -1,5 +1,5 @@ /** - * Check that `value` is identical to type `T`. + * Check that the type of `value` is identical to type `T`. * * @param value - Value that should be identical to type `T`. */ @@ -8,6 +8,26 @@ export const expectType = (value: T) => { // tslint:disable-line:no-unused // Do nothing, the TypeScript compiler handles this for us }; +/** + * Check that the type of `value` is assignable to type `T`. + * + * @param value - Value that should be assignable to type `T`. + */ +// @ts-ignore +export const expectAssignable = (value: T) => { // tslint:disable-line:no-unused + // Do nothing, the TypeScript compiler handles this for us +}; + +/** + * Check that the type of `value` is not assignable to type `T`. + * + * @param value - Value that should not be assignable to type `T`. + */ +// @ts-ignore +export const expectNotAssignable = (value: any) => { // tslint:disable-line:no-unused + // Do nothing, the TypeScript compiler handles this for us +}; + /** * Assert the value to throw an argument error. * diff --git a/source/lib/assertions/handlers/assignability.ts b/source/lib/assertions/handlers/assignability.ts new file mode 100644 index 00000000..51a5f062 --- /dev/null +++ b/source/lib/assertions/handlers/assignability.ts @@ -0,0 +1,39 @@ +import {CallExpression} from '../../../../libraries/typescript/lib/typescript'; +import {TypeChecker} from '../../entities/typescript'; +import {Diagnostic} from '../../interfaces'; +import {makeDiagnostic} from '../../utils'; + +/** + * Verifies that the argument of the assertion is not assignable to the generic type of the assertion. + * + * @param checker - The TypeScript type checker. + * @param nodes - The `expectType` AST nodes. + * @return List of custom diagnostics. + */ +export const isNotAssignable = (checker: TypeChecker, nodes: Set): Diagnostic[] => { + const diagnostics: Diagnostic[] = []; + + if (!nodes) { + return diagnostics; + } + + for (const node of nodes) { + if (!node.typeArguments) { + // Skip if the node does not have generics + continue; + } + + // Retrieve the type to be expected. This is the type inside the generic. + const expectedType = checker.getTypeFromTypeNode(node.typeArguments[0]); + const argumentType = checker.getTypeAtLocation(node.arguments[0]); + + if (checker.isTypeAssignableTo(argumentType, expectedType)) { + /** + * The argument type is assignable to the expected type, we don't want this so add a diagnostic. + */ + diagnostics.push(makeDiagnostic(node, `Argument of type \`${checker.typeToString(argumentType)}\` is assignable to parameter of type \`${checker.typeToString(expectedType)}\`.`)); + } + } + + return diagnostics; +}; diff --git a/source/lib/assertions/handlers/index.ts b/source/lib/assertions/handlers/index.ts index 090cd101..fc77fc57 100644 --- a/source/lib/assertions/handlers/index.ts +++ b/source/lib/assertions/handlers/index.ts @@ -2,3 +2,4 @@ export {Handler} from './handler'; // Handlers export {strictAssertion} from './strict-assertion'; +export {isNotAssignable} from './assignability'; diff --git a/source/lib/assertions/index.ts b/source/lib/assertions/index.ts index 5b9862f9..75bce292 100644 --- a/source/lib/assertions/index.ts +++ b/source/lib/assertions/index.ts @@ -2,15 +2,19 @@ import {CallExpression} from '../../../libraries/typescript/lib/typescript'; import {TypeChecker} from '../entities/typescript'; import {Diagnostic} from '../interfaces'; import {Handler, strictAssertion} from './handlers'; +import {isNotAssignable} from './handlers/assignability'; export enum Assertion { EXPECT_TYPE = 'expectType', - EXPECT_ERROR = 'expectError' + EXPECT_ERROR = 'expectError', + EXPECT_ASSIGNABLE = 'expectAssignable', + EXPECT_NOT_ASSIGNABLE = 'expectNotAssignable' } // List of diagnostic handlers attached to the assertion const assertionHandlers = new Map([ - [Assertion.EXPECT_TYPE, strictAssertion] + [Assertion.EXPECT_TYPE, strictAssertion], + [Assertion.EXPECT_NOT_ASSIGNABLE, isNotAssignable] ]); /** diff --git a/source/test/assignability.ts b/source/test/assignability.ts new file mode 100644 index 00000000..ef95556b --- /dev/null +++ b/source/test/assignability.ts @@ -0,0 +1,21 @@ +import * as path from 'path'; +import test from 'ava'; +import {verify} from './fixtures/utils'; +import tsd from '..'; + +test('assignable', async t => { + const diagnostics = await tsd({cwd: path.join(__dirname, 'fixtures/assignability/assignable')}); + + verify(t, diagnostics, [ + [8, 26, 'error', 'Argument of type \'string\' is not assignable to parameter of type \'boolean\'.'] + ]); +}); + +test('not assignable', async t => { + const diagnostics = await tsd({cwd: path.join(__dirname, 'fixtures/assignability/not-assignable')}); + + verify(t, diagnostics, [ + [4, 0, 'error', 'Argument of type `string` is assignable to parameter of type `string | number`.'], + [5, 0, 'error', 'Argument of type `string` is assignable to parameter of type `any`.'], + ]); +}); diff --git a/source/test/fixtures/assignability/assignable/index.d.ts b/source/test/fixtures/assignability/assignable/index.d.ts new file mode 100644 index 00000000..266914ab --- /dev/null +++ b/source/test/fixtures/assignability/assignable/index.d.ts @@ -0,0 +1,6 @@ +declare const concat: { + (foo: string, bar: string): string; + (foo: number, bar: number): number; +}; + +export default concat; diff --git a/source/test/fixtures/assignability/assignable/index.js b/source/test/fixtures/assignability/assignable/index.js new file mode 100644 index 00000000..f17717f5 --- /dev/null +++ b/source/test/fixtures/assignability/assignable/index.js @@ -0,0 +1,3 @@ +module.exports.default = (foo, bar) => { + return foo + bar; +}; diff --git a/source/test/fixtures/assignability/assignable/index.test-d.ts b/source/test/fixtures/assignability/assignable/index.test-d.ts new file mode 100644 index 00000000..d0583acf --- /dev/null +++ b/source/test/fixtures/assignability/assignable/index.test-d.ts @@ -0,0 +1,8 @@ +import {expectAssignable} from '../../../..'; +import concat from '.'; + +expectAssignable(concat('foo', 'bar')); +expectAssignable(concat(1, 2)); +expectAssignable(concat(1, 2)); + +expectAssignable(concat('unicorn', 'rainbow')); diff --git a/source/test/fixtures/assignability/assignable/package.json b/source/test/fixtures/assignability/assignable/package.json new file mode 100644 index 00000000..de6dc1db --- /dev/null +++ b/source/test/fixtures/assignability/assignable/package.json @@ -0,0 +1,3 @@ +{ + "name": "foo" +} diff --git a/source/test/fixtures/assignability/not-assignable/index.d.ts b/source/test/fixtures/assignability/not-assignable/index.d.ts new file mode 100644 index 00000000..266914ab --- /dev/null +++ b/source/test/fixtures/assignability/not-assignable/index.d.ts @@ -0,0 +1,6 @@ +declare const concat: { + (foo: string, bar: string): string; + (foo: number, bar: number): number; +}; + +export default concat; diff --git a/source/test/fixtures/assignability/not-assignable/index.js b/source/test/fixtures/assignability/not-assignable/index.js new file mode 100644 index 00000000..f17717f5 --- /dev/null +++ b/source/test/fixtures/assignability/not-assignable/index.js @@ -0,0 +1,3 @@ +module.exports.default = (foo, bar) => { + return foo + bar; +}; diff --git a/source/test/fixtures/assignability/not-assignable/index.test-d.ts b/source/test/fixtures/assignability/not-assignable/index.test-d.ts new file mode 100644 index 00000000..39dfbab2 --- /dev/null +++ b/source/test/fixtures/assignability/not-assignable/index.test-d.ts @@ -0,0 +1,8 @@ +import {expectNotAssignable} from '../../../..'; +import concat from '.'; + +expectNotAssignable(concat('foo', 'bar')); +expectNotAssignable(concat('foo', 'bar')); + +expectNotAssignable(concat('unicorn', 'rainbow')); +expectNotAssignable(concat('unicorn', 'rainbow')); diff --git a/source/test/fixtures/assignability/not-assignable/package.json b/source/test/fixtures/assignability/not-assignable/package.json new file mode 100644 index 00000000..de6dc1db --- /dev/null +++ b/source/test/fixtures/assignability/not-assignable/package.json @@ -0,0 +1,3 @@ +{ + "name": "foo" +} diff --git a/source/test/fixtures/utils.ts b/source/test/fixtures/utils.ts new file mode 100644 index 00000000..cab9e6e7 --- /dev/null +++ b/source/test/fixtures/utils.ts @@ -0,0 +1,30 @@ +import {ExecutionContext} from 'ava'; +import {Diagnostic} from '../../lib/interfaces'; + +type Expectation = [number, number, 'error' | 'warning', string, (string | RegExp)?]; + +/** + * Verify a list of diagnostics. + * + * @param t - The AVA execution context. + * @param diagnostics - List of diagnostics to verify. + * @param expectations - Expected diagnostics. + */ +export const verify = (t: ExecutionContext, diagnostics: Diagnostic[], expectations: Expectation[]) => { + t.true(diagnostics.length === expectations.length); + + for (const [index, diagnostic] of diagnostics.entries()) { + t.is(diagnostic.line, expectations[index][0]); + t.is(diagnostic.column, expectations[index][1]); + t.is(diagnostic.severity, expectations[index][2]); + t.is(diagnostic.message, expectations[index][3]); + + const filename = expectations[index][4]; + + if (typeof filename === 'string') { + t.is(diagnostic.fileName, filename); + } else if (typeof filename === 'object') { + t.regex(diagnostic.fileName, filename); + } + } +}; diff --git a/source/test/test.ts b/source/test/test.ts index 487f4cb6..73e19bf3 100644 --- a/source/test/test.ts +++ b/source/test/test.ts @@ -1,35 +1,7 @@ import * as path from 'path'; -import test, {ExecutionContext} from 'ava'; +import test from 'ava'; +import {verify} from './fixtures/utils'; import tsd from '..'; -import {Diagnostic} from '../lib/interfaces'; - -type Expectation = [number, number, 'error' | 'warning', string, (string | RegExp)?]; - -/** - * Verify a list of diagnostics. - * - * @param t - The AVA execution context. - * @param diagnostics - List of diagnostics to verify. - * @param expectations - Expected diagnostics. - */ -const verify = (t: ExecutionContext, diagnostics: Diagnostic[], expectations: Expectation[]) => { - t.true(diagnostics.length === expectations.length); - - for (const [index, diagnostic] of diagnostics.entries()) { - t.is(diagnostic.line, expectations[index][0]); - t.is(diagnostic.column, expectations[index][1]); - t.is(diagnostic.severity, expectations[index][2]); - t.is(diagnostic.message, expectations[index][3]); - - const filename = expectations[index][4]; - - if (typeof filename === 'string') { - t.is(diagnostic.fileName, filename); - } else if (typeof filename === 'object') { - t.regex(diagnostic.fileName, filename); - } - } -}; test('throw if no type definition was found', async t => { await t.throwsAsync(tsd({cwd: path.join(__dirname, 'fixtures/no-tsd')}), 'The type definition `index.d.ts` does not exist. Create one and try again.');