From 1592dfcf34c63062020f7327e6a1c7d99027a4e7 Mon Sep 17 00:00:00 2001 From: skarab42 Date: Fri, 21 Oct 2022 18:25:09 +0200 Subject: [PATCH 01/17] Move tsd assert api in is own directory --- source/index.ts | 2 +- source/lib/assertions/{ => tsd}/assert.ts | 0 source/lib/assertions/{ => tsd}/handlers/assignability.ts | 4 ++-- source/lib/assertions/{ => tsd}/handlers/expect-deprecated.ts | 4 ++-- source/lib/assertions/{ => tsd}/handlers/handler.ts | 2 +- source/lib/assertions/{ => tsd}/handlers/identicality.ts | 4 ++-- source/lib/assertions/{ => tsd}/handlers/index.ts | 0 source/lib/assertions/{ => tsd}/handlers/informational.ts | 4 ++-- source/lib/assertions/{ => tsd}/handlers/strict-assertion.ts | 4 ++-- source/lib/assertions/{ => tsd}/index.ts | 2 +- source/lib/compiler.ts | 2 +- source/lib/parser.ts | 2 +- 12 files changed, 15 insertions(+), 15 deletions(-) rename source/lib/assertions/{ => tsd}/assert.ts (100%) rename source/lib/assertions/{ => tsd}/handlers/assignability.ts (92%) rename source/lib/assertions/{ => tsd}/handlers/expect-deprecated.ts (94%) rename source/lib/assertions/{ => tsd}/handlers/handler.ts (91%) rename source/lib/assertions/{ => tsd}/handlers/identicality.ts (97%) rename source/lib/assertions/{ => tsd}/handlers/index.ts (100%) rename source/lib/assertions/{ => tsd}/handlers/informational.ts (96%) rename source/lib/assertions/{ => tsd}/handlers/strict-assertion.ts (95%) rename source/lib/assertions/{ => tsd}/index.ts (97%) diff --git a/source/index.ts b/source/index.ts index ae6aa66e..4aa6c44b 100644 --- a/source/index.ts +++ b/source/index.ts @@ -1,4 +1,4 @@ import tsd from './lib'; -export * from './lib/assertions/assert'; +export * from './lib/assertions/tsd/assert'; export default tsd; diff --git a/source/lib/assertions/assert.ts b/source/lib/assertions/tsd/assert.ts similarity index 100% rename from source/lib/assertions/assert.ts rename to source/lib/assertions/tsd/assert.ts diff --git a/source/lib/assertions/handlers/assignability.ts b/source/lib/assertions/tsd/handlers/assignability.ts similarity index 92% rename from source/lib/assertions/handlers/assignability.ts rename to source/lib/assertions/tsd/handlers/assignability.ts index a1ad6214..d1296fab 100644 --- a/source/lib/assertions/handlers/assignability.ts +++ b/source/lib/assertions/tsd/handlers/assignability.ts @@ -1,6 +1,6 @@ import {CallExpression, TypeChecker} from '@tsd/typescript'; -import {Diagnostic} from '../../interfaces'; -import {makeDiagnostic} from '../../utils'; +import {Diagnostic} from '../../../interfaces'; +import {makeDiagnostic} from '../../../utils'; /** * Asserts that the argument of the assertion is not assignable to the generic type of the assertion. diff --git a/source/lib/assertions/handlers/expect-deprecated.ts b/source/lib/assertions/tsd/handlers/expect-deprecated.ts similarity index 94% rename from source/lib/assertions/handlers/expect-deprecated.ts rename to source/lib/assertions/tsd/handlers/expect-deprecated.ts index 138346ca..96645e75 100644 --- a/source/lib/assertions/handlers/expect-deprecated.ts +++ b/source/lib/assertions/tsd/handlers/expect-deprecated.ts @@ -1,7 +1,7 @@ import {JSDocTagInfo} from '@tsd/typescript'; -import {Diagnostic} from '../../interfaces'; +import {Diagnostic} from '../../../interfaces'; import {Handler} from './handler'; -import {makeDiagnostic, tsutils} from '../../utils'; +import {makeDiagnostic, tsutils} from '../../../utils'; interface Options { filter(tags: Map): boolean; diff --git a/source/lib/assertions/handlers/handler.ts b/source/lib/assertions/tsd/handlers/handler.ts similarity index 91% rename from source/lib/assertions/handlers/handler.ts rename to source/lib/assertions/tsd/handlers/handler.ts index e3f7e089..be10f21d 100644 --- a/source/lib/assertions/handlers/handler.ts +++ b/source/lib/assertions/tsd/handlers/handler.ts @@ -1,5 +1,5 @@ import {CallExpression, TypeChecker} from '@tsd/typescript'; -import {Diagnostic} from '../../interfaces'; +import {Diagnostic} from '../../../interfaces'; /** * A handler is a method which accepts the TypeScript type checker together with a set of assertion nodes. The type checker diff --git a/source/lib/assertions/handlers/identicality.ts b/source/lib/assertions/tsd/handlers/identicality.ts similarity index 97% rename from source/lib/assertions/handlers/identicality.ts rename to source/lib/assertions/tsd/handlers/identicality.ts index f375c134..ef65a34b 100644 --- a/source/lib/assertions/handlers/identicality.ts +++ b/source/lib/assertions/tsd/handlers/identicality.ts @@ -1,6 +1,6 @@ import {CallExpression, TypeChecker, TypeFlags} from '@tsd/typescript'; -import {Diagnostic} from '../../interfaces'; -import {makeDiagnostic} from '../../utils'; +import {Diagnostic} from '../../../interfaces'; +import {makeDiagnostic} from '../../../utils'; /** * Asserts that the argument of the assertion is identical to the generic type of the assertion. diff --git a/source/lib/assertions/handlers/index.ts b/source/lib/assertions/tsd/handlers/index.ts similarity index 100% rename from source/lib/assertions/handlers/index.ts rename to source/lib/assertions/tsd/handlers/index.ts diff --git a/source/lib/assertions/handlers/informational.ts b/source/lib/assertions/tsd/handlers/informational.ts similarity index 96% rename from source/lib/assertions/handlers/informational.ts rename to source/lib/assertions/tsd/handlers/informational.ts index a7f08f3e..bd5af404 100644 --- a/source/lib/assertions/handlers/informational.ts +++ b/source/lib/assertions/tsd/handlers/informational.ts @@ -1,6 +1,6 @@ import {CallExpression, TypeChecker, TypeFormatFlags} from '@tsd/typescript'; -import {Diagnostic} from '../../interfaces'; -import {makeDiagnostic, tsutils} from '../../utils'; +import {Diagnostic} from '../../../interfaces'; +import {makeDiagnostic, tsutils} from '../../../utils'; /** * Default formatting flags set by TS plus the {@link TypeFormatFlags.NoTruncation NoTruncation} flag. diff --git a/source/lib/assertions/handlers/strict-assertion.ts b/source/lib/assertions/tsd/handlers/strict-assertion.ts similarity index 95% rename from source/lib/assertions/handlers/strict-assertion.ts rename to source/lib/assertions/tsd/handlers/strict-assertion.ts index 5ecc096e..b2f83cfc 100644 --- a/source/lib/assertions/handlers/strict-assertion.ts +++ b/source/lib/assertions/tsd/handlers/strict-assertion.ts @@ -1,6 +1,6 @@ import {CallExpression, TypeChecker} from '@tsd/typescript'; -import {Diagnostic} from '../../interfaces'; -import {makeDiagnostic} from '../../utils'; +import {Diagnostic} from '../../../interfaces'; +import {makeDiagnostic} from '../../../utils'; /** * Performs strict type assertion between the argument if the assertion, and the generic type of the assertion. diff --git a/source/lib/assertions/index.ts b/source/lib/assertions/tsd/index.ts similarity index 97% rename from source/lib/assertions/index.ts rename to source/lib/assertions/tsd/index.ts index 6cf6f8e9..35bad84d 100644 --- a/source/lib/assertions/index.ts +++ b/source/lib/assertions/tsd/index.ts @@ -1,5 +1,5 @@ import {CallExpression, TypeChecker} from '@tsd/typescript'; -import {Diagnostic} from '../interfaces'; +import {Diagnostic} from '../../interfaces'; import { Handler, isIdentical, diff --git a/source/lib/compiler.ts b/source/lib/compiler.ts index d34476a8..a0dce4da 100644 --- a/source/lib/compiler.ts +++ b/source/lib/compiler.ts @@ -5,7 +5,7 @@ import { } from '@tsd/typescript'; import {ExpectedError, extractAssertions, parseErrorAssertionToLocation} from './parser'; import {Diagnostic, DiagnosticCode, Context, Location} from './interfaces'; -import {handle} from './assertions'; +import {handle} from './assertions/tsd'; // List of diagnostic codes that should be ignored in general const ignoredDiagnostics = new Set([ diff --git a/source/lib/parser.ts b/source/lib/parser.ts index 8ad952f4..3e90c331 100644 --- a/source/lib/parser.ts +++ b/source/lib/parser.ts @@ -1,5 +1,5 @@ import {Program, Node, CallExpression, forEachChild, isCallExpression, isPropertyAccessExpression, SymbolFlags} from '@tsd/typescript'; -import {Assertion} from './assertions'; +import {Assertion} from './assertions/tsd'; import {Location, Diagnostic} from './interfaces'; const assertionFnNames = new Set(Object.values(Assertion)); From 0989aa8d942f01e1e8faafaead9ba460ced73acc Mon Sep 17 00:00:00 2001 From: skarab42 Date: Sat, 22 Oct 2022 10:51:57 +0200 Subject: [PATCH 02/17] Add Jest like api parser --- source/index.ts | 2 + source/lib/assertions/jest-like/assert.ts | 41 ++++++++++++ .../assertions/jest-like/handlers/index.ts | 2 + .../jest-like/handlers/is-identical.ts | 18 ++++++ .../jest-like/handlers/is-not-identical.ts | 18 ++++++ source/lib/assertions/jest-like/index.ts | 53 ++++++++++++++++ source/lib/compiler.ts | 7 ++- source/lib/parser.ts | 63 ++++++++++++++++++- source/test/fixtures/jest-like-api/index.d.ts | 0 source/test/fixtures/jest-like-api/index.js | 0 .../fixtures/jest-like-api/index.test-d.ts | 12 ++++ .../test/fixtures/jest-like-api/package.json | 3 + source/test/jest-like-api.ts | 15 +++++ 13 files changed, 230 insertions(+), 4 deletions(-) create mode 100644 source/lib/assertions/jest-like/assert.ts create mode 100644 source/lib/assertions/jest-like/handlers/index.ts create mode 100644 source/lib/assertions/jest-like/handlers/is-identical.ts create mode 100644 source/lib/assertions/jest-like/handlers/is-not-identical.ts create mode 100644 source/lib/assertions/jest-like/index.ts create mode 100644 source/test/fixtures/jest-like-api/index.d.ts create mode 100644 source/test/fixtures/jest-like-api/index.js create mode 100644 source/test/fixtures/jest-like-api/index.test-d.ts create mode 100644 source/test/fixtures/jest-like-api/package.json create mode 100644 source/test/jest-like-api.ts diff --git a/source/index.ts b/source/index.ts index 4aa6c44b..865dc95c 100644 --- a/source/index.ts +++ b/source/index.ts @@ -1,4 +1,6 @@ import tsd from './lib'; export * from './lib/assertions/tsd/assert'; +export * from './lib/assertions/jest-like/assert'; + export default tsd; diff --git a/source/lib/assertions/jest-like/assert.ts b/source/lib/assertions/jest-like/assert.ts new file mode 100644 index 00000000..72072af9 --- /dev/null +++ b/source/lib/assertions/jest-like/assert.ts @@ -0,0 +1,41 @@ +/* eslint-disable @typescript-eslint/no-empty-function */ +/* eslint-disable @typescript-eslint/no-unused-vars */ + +/** + * Test that target type is identical to expected type. + * + * @template TargetType - The expected type that will be compared with another type. + * @param targetValue - The expected value whose type will be compared with another type. + */ +// @ts-expect-error no-unused-vars +function identicalTo(targetValue?: TargetType) {} + +/** + * Test that target type is not identical to expected type. + * + * @template TargetType - The expected type that will be compared with another type. + * @param targetValue - The expected value whose type will be compared with another type. + */ +// @ts-expect-error no-unused-vars +function notIdenticalTo(targetValue?: TargetType) {} + +// Construct the public API. +const assertTypeAPI = { + identicalTo, + not: { + identicalTo: notIdenticalTo, + }, +}; + +type AssertTypeAPI = typeof assertTypeAPI; + +/** + * Create a type assertion holder from the type provided in the first generic parameter or from the type of the first argument. + * + * @template ExpectedType - The expected type that will be compared with another type. + * @param expectedValue - The expected value whose type will be compared with another type. + */ +// @ts-expect-error no-unused-vars +export function assertType(expectedValue?: unknown): AssertTypeAPI { + return assertTypeAPI; +} diff --git a/source/lib/assertions/jest-like/handlers/index.ts b/source/lib/assertions/jest-like/handlers/index.ts new file mode 100644 index 00000000..a8762e1a --- /dev/null +++ b/source/lib/assertions/jest-like/handlers/index.ts @@ -0,0 +1,2 @@ +export {isIdentical} from './is-identical'; +export {isNotIdentical} from './is-not-identical'; diff --git a/source/lib/assertions/jest-like/handlers/is-identical.ts b/source/lib/assertions/jest-like/handlers/is-identical.ts new file mode 100644 index 00000000..9de2fcd4 --- /dev/null +++ b/source/lib/assertions/jest-like/handlers/is-identical.ts @@ -0,0 +1,18 @@ +import {TypeChecker} from '@tsd/typescript'; +import {Diagnostic} from '../../../interfaces'; +import {JestLikeAssertionNodes} from '..'; + +/** + * Asserts that the argument of the assertion is not identical 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 isIdentical = (_checker: TypeChecker, nodes: JestLikeAssertionNodes): Diagnostic[] => { + const diagnostics: Diagnostic[] = []; + + console.log('>>>> isIdentical', nodes); + + return diagnostics; +}; diff --git a/source/lib/assertions/jest-like/handlers/is-not-identical.ts b/source/lib/assertions/jest-like/handlers/is-not-identical.ts new file mode 100644 index 00000000..9f8e7274 --- /dev/null +++ b/source/lib/assertions/jest-like/handlers/is-not-identical.ts @@ -0,0 +1,18 @@ +import {TypeChecker} from '@tsd/typescript'; +import {Diagnostic} from '../../../interfaces'; +import {JestLikeAssertionNodes} from '..'; + +/** + * Asserts that the argument of the assertion is identical 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 isNotIdentical = (_checker: TypeChecker, nodes: JestLikeAssertionNodes): Diagnostic[] => { + const diagnostics: Diagnostic[] = []; + + console.log('>>>> isNotIdentical', nodes); + + return diagnostics; +}; diff --git a/source/lib/assertions/jest-like/index.ts b/source/lib/assertions/jest-like/index.ts new file mode 100644 index 00000000..6c96cd84 --- /dev/null +++ b/source/lib/assertions/jest-like/index.ts @@ -0,0 +1,53 @@ +import {CallExpression, PropertyAccessExpression, TypeChecker} from '@tsd/typescript'; +import {Diagnostic} from '../../interfaces'; +import {isIdentical, isNotIdentical} from '../jest-like/handlers'; + +export type JestLikeAssertionNodes = Set<[CallExpression, PropertyAccessExpression]>; + +/** + * A handler is a method which accepts the TypeScript type checker together with a set of assertion nodes. The type checker + * can be used to retrieve extra type information from these nodes in order to determine a list of diagnostics. + * + * @param typeChecker - The TypeScript type checker. + * @param nodes - List of nodes. + * @returns List of diagnostics. + */ +export type JestLikeHandler = (typeChecker: TypeChecker, nodes: JestLikeAssertionNodes) => Diagnostic[]; + +export type JestLikeAssertionHandlers = Map; +export type JestLikeAssertions = Map; + +export enum JestLikeAssertion { + IDENTICAL_TO = 'identicalTo', + NOT_IDENTICAL_TO = 'notIdenticalTo', +} + +// List of diagnostic handlers attached to the assertion +const assertionHandlers: JestLikeAssertionHandlers = new Map([ + [JestLikeAssertion.IDENTICAL_TO, isIdentical], + [JestLikeAssertion.NOT_IDENTICAL_TO, isNotIdentical], +]); + +/** + * Returns a list of diagnostics based on the assertions provided. + * + * @param typeChecker - The TypeScript type checker. + * @param assertions - Assertion map with the key being the assertion, and the value the list of all those assertion nodes. + * @returns List of diagnostics. + */ +export const jestLikeHandle = (typeChecker: TypeChecker, assertions: JestLikeAssertions): Diagnostic[] => { + const diagnostics: Diagnostic[] = []; + + for (const [assertion, nodes] of assertions) { + const handler = assertionHandlers.get(assertion); + + if (!handler) { + // Ignore these assertions as no handler is found + continue; + } + + diagnostics.push(...handler(typeChecker, nodes)); + } + + return diagnostics; +}; diff --git a/source/lib/compiler.ts b/source/lib/compiler.ts index a0dce4da..22bb480d 100644 --- a/source/lib/compiler.ts +++ b/source/lib/compiler.ts @@ -6,6 +6,7 @@ import { import {ExpectedError, extractAssertions, parseErrorAssertionToLocation} from './parser'; import {Diagnostic, DiagnosticCode, Context, Location} from './interfaces'; import {handle} from './assertions/tsd'; +import {jestLikeHandle} from './assertions/jest-like'; // List of diagnostic codes that should be ignored in general const ignoredDiagnostics = new Set([ @@ -96,9 +97,11 @@ export const getDiagnostics = (context: Context): Diagnostic[] => { const assertions = extractAssertions(program); - diagnostics.push(...handle(program.getTypeChecker(), assertions)); + diagnostics.push(...assertions.diagnostics); + diagnostics.push(...handle(program.getTypeChecker(), assertions.assertions)); + diagnostics.push(...jestLikeHandle(program.getTypeChecker(), assertions.jestLikeAssertions)); - const expectedErrors = parseErrorAssertionToLocation(assertions); + const expectedErrors = parseErrorAssertionToLocation(assertions.assertions); const expectedErrorsLocationsWithFoundDiagnostics: Location[] = []; for (const diagnostic of tsDiagnostics) { diff --git a/source/lib/parser.ts b/source/lib/parser.ts index 3e90c331..58ea073b 100644 --- a/source/lib/parser.ts +++ b/source/lib/parser.ts @@ -1,17 +1,27 @@ import {Program, Node, CallExpression, forEachChild, isCallExpression, isPropertyAccessExpression, SymbolFlags} from '@tsd/typescript'; +import {JestLikeAssertion, JestLikeAssertions} from './assertions/jest-like'; import {Assertion} from './assertions/tsd'; import {Location, Diagnostic} from './interfaces'; +import {makeDiagnostic} from './utils'; const assertionFnNames = new Set(Object.values(Assertion)); +type Assertions = { + assertions: Map>; + jestLikeAssertions: JestLikeAssertions; + diagnostics: Diagnostic[]; +}; + /** * Extract all assertions. * * @param program - TypeScript program. */ -export const extractAssertions = (program: Program): Map> => { +export const extractAssertions = (program: Program): Assertions => { + const jestLikeAssertions: JestLikeAssertions = new Map(); const assertions = new Map>(); const checker = program.getTypeChecker(); + const diagnostics: Diagnostic[] = []; /** * Checks if the given node is semantically valid and is an assertion. @@ -47,6 +57,55 @@ export const extractAssertions = (program: Program): Map()`.')); + return; + } + + const assertMethodSymbol = checker.getSymbolAtLocation(targetNode); + + if (!assertMethodSymbol) { + diagnostics.push(makeDiagnostic(targetNode, 'Missing right side method, expected something like `assertType(\'hello\').assignableTo()`.')); + return; + } + + const assertMethodName = assertMethodSymbol.getName(); + + if (assertMethodName !== 'not') { + const nodes = jestLikeAssertions.get(assertMethodName as JestLikeAssertion) ?? new Set(); + + nodes.add([expectedNode, targetNode]); + + jestLikeAssertions.set(assertMethodName as JestLikeAssertion, nodes); + return; + } + + const maybeTargetNode = targetNode.parent; + + if (!isPropertyAccessExpression(maybeTargetNode)) { + diagnostics.push(makeDiagnostic(maybeTargetNode, 'Missing right side method, expected something like `assertType(\'hello\').not.assignableTo()`.')); + return; + } + + const maybeTargetType = checker.getTypeAtLocation(maybeTargetNode.name); + const maybeTargetName = maybeTargetType.symbol.getName() as JestLikeAssertion; + + const nodes = jestLikeAssertions.get(maybeTargetName) ?? new Set(); + + nodes.add([expectedNode, maybeTargetNode]); + + jestLikeAssertions.set(maybeTargetName, nodes); } /** @@ -64,7 +123,7 @@ export const extractAssertions = (program: Program): Map; diff --git a/source/test/fixtures/jest-like-api/index.d.ts b/source/test/fixtures/jest-like-api/index.d.ts new file mode 100644 index 00000000..e69de29b diff --git a/source/test/fixtures/jest-like-api/index.js b/source/test/fixtures/jest-like-api/index.js new file mode 100644 index 00000000..e69de29b diff --git a/source/test/fixtures/jest-like-api/index.test-d.ts b/source/test/fixtures/jest-like-api/index.test-d.ts new file mode 100644 index 00000000..3e5912fa --- /dev/null +++ b/source/test/fixtures/jest-like-api/index.test-d.ts @@ -0,0 +1,12 @@ +import {assertType} from '../../..'; + +assertType().identicalTo(); +assertType().not.identicalTo(); + +assertType(); +assertType().not; + +// @ts-expect-error +assertType().prout(); +// @ts-expect-error +assertType().nop.identicalTo(); diff --git a/source/test/fixtures/jest-like-api/package.json b/source/test/fixtures/jest-like-api/package.json new file mode 100644 index 00000000..de6dc1db --- /dev/null +++ b/source/test/fixtures/jest-like-api/package.json @@ -0,0 +1,3 @@ +{ + "name": "foo" +} diff --git a/source/test/jest-like-api.ts b/source/test/jest-like-api.ts new file mode 100644 index 00000000..32dc7fc9 --- /dev/null +++ b/source/test/jest-like-api.ts @@ -0,0 +1,15 @@ +import tsd from '..'; +import test from 'ava'; +import path from 'path'; +import {verify} from './fixtures/utils'; + +test('jest like API', async t => { + const diagnostics = await tsd({cwd: path.join(__dirname, 'fixtures/jest-like-api')}); + + verify(t, diagnostics, [ + [6, 0, 'error', 'Missing right side method, expected something like `assertType(\'hello\').assignableTo()`.'], + [7, 0, 'error', 'Missing right side method, expected something like `assertType(\'hello\').not.assignableTo()`.'], + [10, 0, 'error', 'Missing right side method, expected something like `assertType(\'hello\').assignableTo()`.'], + [12, 0, 'error', 'Missing right side method, expected something like `assertType(\'hello\').assignableTo()`.'], + ]); +}); From 482cb5b9cf734d297fc5ecf215667ad169bbd360 Mon Sep 17 00:00:00 2001 From: skarab42 Date: Sat, 22 Oct 2022 13:01:43 +0200 Subject: [PATCH 03/17] Add identicality handlers --- .../jest-like/handlers/is-identical.ts | 30 +++++++++++++- .../jest-like/handlers/is-not-identical.ts | 30 +++++++++++++- source/lib/assertions/jest-like/index.ts | 4 +- source/lib/assertions/jest-like/util.ts | 37 ++++++++++++++++++ source/lib/parser.ts | 4 +- .../{ => identicality}/index.d.ts | 0 .../jest-like-api/{ => identicality}/index.js | 0 .../identicality/index.test-d.ts | 26 +++++++++++++ .../{ => identicality}/package.json | 0 .../fixtures/jest-like-api/index.test-d.ts | 12 ------ .../jest-like-api/parser-error/index.d.ts | 0 .../jest-like-api/parser-error/index.js | 0 .../parser-error/index.test-d.ts | 22 +++++++++++ .../jest-like-api/parser-error/package.json | 3 ++ source/test/jest-like-api.ts | 39 ++++++++++++++++--- 15 files changed, 182 insertions(+), 25 deletions(-) create mode 100644 source/lib/assertions/jest-like/util.ts rename source/test/fixtures/jest-like-api/{ => identicality}/index.d.ts (100%) rename source/test/fixtures/jest-like-api/{ => identicality}/index.js (100%) create mode 100644 source/test/fixtures/jest-like-api/identicality/index.test-d.ts rename source/test/fixtures/jest-like-api/{ => identicality}/package.json (100%) delete mode 100644 source/test/fixtures/jest-like-api/index.test-d.ts create mode 100644 source/test/fixtures/jest-like-api/parser-error/index.d.ts create mode 100644 source/test/fixtures/jest-like-api/parser-error/index.js create mode 100644 source/test/fixtures/jest-like-api/parser-error/index.test-d.ts create mode 100644 source/test/fixtures/jest-like-api/parser-error/package.json diff --git a/source/lib/assertions/jest-like/handlers/is-identical.ts b/source/lib/assertions/jest-like/handlers/is-identical.ts index 9de2fcd4..0b2a352e 100644 --- a/source/lib/assertions/jest-like/handlers/is-identical.ts +++ b/source/lib/assertions/jest-like/handlers/is-identical.ts @@ -1,6 +1,8 @@ import {TypeChecker} from '@tsd/typescript'; import {Diagnostic} from '../../../interfaces'; +import {makeDiagnostic} from '../../../utils'; import {JestLikeAssertionNodes} from '..'; +import {getTypes} from '../util'; /** * Asserts that the argument of the assertion is not identical to the generic type of the assertion. @@ -9,10 +11,34 @@ import {JestLikeAssertionNodes} from '..'; * @param nodes - The `expectType` AST nodes. * @return List of custom diagnostics. */ -export const isIdentical = (_checker: TypeChecker, nodes: JestLikeAssertionNodes): Diagnostic[] => { +export const isIdentical = (checker: TypeChecker, nodes: JestLikeAssertionNodes): Diagnostic[] => { const diagnostics: Diagnostic[] = []; - console.log('>>>> isIdentical', nodes); + if (!nodes) { + return diagnostics; + } + + for (const node of nodes) { + const [expectedNode, targetNode] = node; + + const expected = getTypes(expectedNode, checker); + + if (expected.diagnostic) { + diagnostics.push(expected.diagnostic); + continue; + } + + const target = getTypes(targetNode, checker); + + if (target.diagnostic) { + diagnostics.push(target.diagnostic); + continue; + } + + if (!checker.isTypeIdenticalTo(expected.type, target.type)) { + diagnostics.push(makeDiagnostic(expectedNode, `Parameter type \`${checker.typeToString(expected.type)}\` is not identical to argument type \`${checker.typeToString(target.type)}\`.`)); + } + } return diagnostics; }; diff --git a/source/lib/assertions/jest-like/handlers/is-not-identical.ts b/source/lib/assertions/jest-like/handlers/is-not-identical.ts index 9f8e7274..5abad176 100644 --- a/source/lib/assertions/jest-like/handlers/is-not-identical.ts +++ b/source/lib/assertions/jest-like/handlers/is-not-identical.ts @@ -1,6 +1,8 @@ import {TypeChecker} from '@tsd/typescript'; import {Diagnostic} from '../../../interfaces'; +import {makeDiagnostic} from '../../../utils'; import {JestLikeAssertionNodes} from '..'; +import {getTypes} from '../util'; /** * Asserts that the argument of the assertion is identical to the generic type of the assertion. @@ -9,10 +11,34 @@ import {JestLikeAssertionNodes} from '..'; * @param nodes - The `expectType` AST nodes. * @return List of custom diagnostics. */ -export const isNotIdentical = (_checker: TypeChecker, nodes: JestLikeAssertionNodes): Diagnostic[] => { +export const isNotIdentical = (checker: TypeChecker, nodes: JestLikeAssertionNodes): Diagnostic[] => { const diagnostics: Diagnostic[] = []; - console.log('>>>> isNotIdentical', nodes); + if (!nodes) { + return diagnostics; + } + + for (const node of nodes) { + const [expectedNode, targetNode] = node; + + const expected = getTypes(expectedNode, checker); + + if (expected.diagnostic) { + diagnostics.push(expected.diagnostic); + continue; + } + + const target = getTypes(targetNode, checker); + + if (target.diagnostic) { + diagnostics.push(target.diagnostic); + continue; + } + + if (checker.isTypeIdenticalTo(expected.type, target.type)) { + diagnostics.push(makeDiagnostic(expectedNode, `Parameter type \`${checker.typeToString(expected.type)}\` is identical to argument type \`${checker.typeToString(target.type)}\`.`)); + } + } return diagnostics; }; diff --git a/source/lib/assertions/jest-like/index.ts b/source/lib/assertions/jest-like/index.ts index 6c96cd84..a27e6231 100644 --- a/source/lib/assertions/jest-like/index.ts +++ b/source/lib/assertions/jest-like/index.ts @@ -1,8 +1,8 @@ -import {CallExpression, PropertyAccessExpression, TypeChecker} from '@tsd/typescript'; +import {CallExpression, TypeChecker} from '@tsd/typescript'; import {Diagnostic} from '../../interfaces'; import {isIdentical, isNotIdentical} from '../jest-like/handlers'; -export type JestLikeAssertionNodes = Set<[CallExpression, PropertyAccessExpression]>; +export type JestLikeAssertionNodes = Set<[CallExpression, CallExpression]>; /** * A handler is a method which accepts the TypeScript type checker together with a set of assertion nodes. The type checker diff --git a/source/lib/assertions/jest-like/util.ts b/source/lib/assertions/jest-like/util.ts new file mode 100644 index 00000000..b5b35056 --- /dev/null +++ b/source/lib/assertions/jest-like/util.ts @@ -0,0 +1,37 @@ +import {CallExpression, Node, Type, TypeChecker} from '@tsd/typescript'; +import {Diagnostic} from '../../interfaces'; +import {makeDiagnostic} from '../../utils'; + +type Types = + | {type: Type; argument: Node; diagnostic?: never} + | {diagnostic: Diagnostic; type?: never; argument?: never}; + +export function getTypes(node: CallExpression, typeChecker: TypeChecker): Types { + let type: Type | undefined; + let value: Type | undefined; + + const typeArgument = node.typeArguments?.[0]; + const valueArgument = node.arguments[0]; + + if (typeArgument) { + type = typeChecker.getTypeFromTypeNode(typeArgument); + } + + if (valueArgument) { + value = typeChecker.getTypeAtLocation(valueArgument); + } + + if (type && value) { + return {diagnostic: makeDiagnostic(typeArgument ?? node, 'Do not provide a generic type and an argument value at the same time.')}; + } + + if (type && typeArgument) { + return {type, argument: typeArgument}; + } + + if (value && valueArgument) { + return {type: value, argument: valueArgument}; + } + + return {diagnostic: makeDiagnostic(node, 'A generic type or an argument value is required.')}; +} diff --git a/source/lib/parser.ts b/source/lib/parser.ts index 58ea073b..49bb34e8 100644 --- a/source/lib/parser.ts +++ b/source/lib/parser.ts @@ -85,7 +85,7 @@ export const extractAssertions = (program: Program): Assertions => { if (assertMethodName !== 'not') { const nodes = jestLikeAssertions.get(assertMethodName as JestLikeAssertion) ?? new Set(); - nodes.add([expectedNode, targetNode]); + nodes.add([expectedNode, targetNode.parent as CallExpression]); jestLikeAssertions.set(assertMethodName as JestLikeAssertion, nodes); return; @@ -103,7 +103,7 @@ export const extractAssertions = (program: Program): Assertions => { const nodes = jestLikeAssertions.get(maybeTargetName) ?? new Set(); - nodes.add([expectedNode, maybeTargetNode]); + nodes.add([expectedNode, maybeTargetNode.parent as CallExpression]); jestLikeAssertions.set(maybeTargetName, nodes); } diff --git a/source/test/fixtures/jest-like-api/index.d.ts b/source/test/fixtures/jest-like-api/identicality/index.d.ts similarity index 100% rename from source/test/fixtures/jest-like-api/index.d.ts rename to source/test/fixtures/jest-like-api/identicality/index.d.ts diff --git a/source/test/fixtures/jest-like-api/index.js b/source/test/fixtures/jest-like-api/identicality/index.js similarity index 100% rename from source/test/fixtures/jest-like-api/index.js rename to source/test/fixtures/jest-like-api/identicality/index.js diff --git a/source/test/fixtures/jest-like-api/identicality/index.test-d.ts b/source/test/fixtures/jest-like-api/identicality/index.test-d.ts new file mode 100644 index 00000000..e18dd224 --- /dev/null +++ b/source/test/fixtures/jest-like-api/identicality/index.test-d.ts @@ -0,0 +1,26 @@ +import {assertType} from '../../../..'; + +declare const fooString: string; +declare const fooNumber: number; + +// Should pass +assertType().identicalTo(); +assertType().identicalTo(fooString); +assertType(fooString).identicalTo(); +assertType(fooString).identicalTo(fooString); + +assertType().not.identicalTo(); +assertType().not.identicalTo(fooString); +assertType(fooString).not.identicalTo(); +assertType(fooString).not.identicalTo(fooNumber); + +// Shoul fail +assertType().identicalTo(); +assertType().identicalTo(fooString); +assertType(fooString).identicalTo(); +assertType(fooString).identicalTo(fooNumber); + +assertType().not.identicalTo(); +assertType().not.identicalTo(fooString); +assertType(fooString).not.identicalTo(); +assertType(fooString).not.identicalTo(fooString); diff --git a/source/test/fixtures/jest-like-api/package.json b/source/test/fixtures/jest-like-api/identicality/package.json similarity index 100% rename from source/test/fixtures/jest-like-api/package.json rename to source/test/fixtures/jest-like-api/identicality/package.json diff --git a/source/test/fixtures/jest-like-api/index.test-d.ts b/source/test/fixtures/jest-like-api/index.test-d.ts deleted file mode 100644 index 3e5912fa..00000000 --- a/source/test/fixtures/jest-like-api/index.test-d.ts +++ /dev/null @@ -1,12 +0,0 @@ -import {assertType} from '../../..'; - -assertType().identicalTo(); -assertType().not.identicalTo(); - -assertType(); -assertType().not; - -// @ts-expect-error -assertType().prout(); -// @ts-expect-error -assertType().nop.identicalTo(); diff --git a/source/test/fixtures/jest-like-api/parser-error/index.d.ts b/source/test/fixtures/jest-like-api/parser-error/index.d.ts new file mode 100644 index 00000000..e69de29b diff --git a/source/test/fixtures/jest-like-api/parser-error/index.js b/source/test/fixtures/jest-like-api/parser-error/index.js new file mode 100644 index 00000000..e69de29b diff --git a/source/test/fixtures/jest-like-api/parser-error/index.test-d.ts b/source/test/fixtures/jest-like-api/parser-error/index.test-d.ts new file mode 100644 index 00000000..65cf601c --- /dev/null +++ b/source/test/fixtures/jest-like-api/parser-error/index.test-d.ts @@ -0,0 +1,22 @@ +import {assertType} from '../../../..'; + +// Must fail on invalid syntax +assertType(); +assertType().not; + +// @ts-expect-error +assertType().prout(); +// @ts-expect-error +assertType().nop.identicalTo(); + +// Shoul fail on missing generic type or argument +assertType().identicalTo(); +assertType().identicalTo(); +assertType().not.identicalTo(); +assertType().not.identicalTo(); + +// Shoul fail if both generic type and argument was provided +assertType('foo').identicalTo(); +assertType().identicalTo('foo'); +assertType('foo').not.identicalTo(); +assertType().not.identicalTo('foo'); diff --git a/source/test/fixtures/jest-like-api/parser-error/package.json b/source/test/fixtures/jest-like-api/parser-error/package.json new file mode 100644 index 00000000..de6dc1db --- /dev/null +++ b/source/test/fixtures/jest-like-api/parser-error/package.json @@ -0,0 +1,3 @@ +{ + "name": "foo" +} diff --git a/source/test/jest-like-api.ts b/source/test/jest-like-api.ts index 32dc7fc9..97cbdb2a 100644 --- a/source/test/jest-like-api.ts +++ b/source/test/jest-like-api.ts @@ -3,13 +3,42 @@ import test from 'ava'; import path from 'path'; import {verify} from './fixtures/utils'; -test('jest like API', async t => { - const diagnostics = await tsd({cwd: path.join(__dirname, 'fixtures/jest-like-api')}); +test('jest like API parser error', async t => { + const diagnostics = await tsd({cwd: path.join(__dirname, 'fixtures/jest-like-api/parser-error')}); verify(t, diagnostics, [ - [6, 0, 'error', 'Missing right side method, expected something like `assertType(\'hello\').assignableTo()`.'], - [7, 0, 'error', 'Missing right side method, expected something like `assertType(\'hello\').not.assignableTo()`.'], + [4, 0, 'error', 'Missing right side method, expected something like `assertType(\'hello\').assignableTo()`.'], + [5, 0, 'error', 'Missing right side method, expected something like `assertType(\'hello\').not.assignableTo()`.'], + [8, 0, 'error', 'Missing right side method, expected something like `assertType(\'hello\').assignableTo()`.'], [10, 0, 'error', 'Missing right side method, expected something like `assertType(\'hello\').assignableTo()`.'], - [12, 0, 'error', 'Missing right side method, expected something like `assertType(\'hello\').assignableTo()`.'], + [13, 0, 'error', 'A generic type or an argument value is required.'], + [14, 0, 'error', 'A generic type or an argument value is required.'], + [19, 11, 'error', 'Do not provide a generic type and an argument value at the same time.'], + [20, 33, 'error', 'Do not provide a generic type and an argument value at the same time.'], + [15, 0, 'error', 'A generic type or an argument value is required.'], + [16, 0, 'error', 'A generic type or an argument value is required.'], + [21, 11, 'error', 'Do not provide a generic type and an argument value at the same time.'], + [22, 37, 'error', 'Do not provide a generic type and an argument value at the same time.'], ]); }); + +test('jest like API identicality', async t => { + const diagnostics = await tsd({cwd: path.join(__dirname, 'fixtures/jest-like-api/identicality')}); + + verify(t, diagnostics, [ + [18, 0, 'error', 'Parameter type `string` is not identical to argument type `number`.'], + [19, 0, 'error', 'Parameter type `number` is not identical to argument type `string`.'], + [20, 0, 'error', 'Parameter type `string` is not identical to argument type `number`.'], + [21, 0, 'error', 'Parameter type `string` is not identical to argument type `number`.'], + [23, 0, 'error', 'Parameter type `string` is identical to argument type `string`.'], + [24, 0, 'error', 'Parameter type `string` is identical to argument type `string`.'], + [25, 0, 'error', 'Parameter type `string` is identical to argument type `string`.'], + [26, 0, 'error', 'Parameter type `string` is identical to argument type `string`.'], + ]); +}); + +// Debug +// test('debug', async () => { +// const diagnostics = await tsd({cwd: path.join(__dirname, 'fixtures/jest-like-api')}); +// console.log(diagnostics); +// }); From 8aa2954712d84526d399674533cff42696af8891 Mon Sep 17 00:00:00 2001 From: skarab42 Date: Sat, 22 Oct 2022 14:22:40 +0200 Subject: [PATCH 04/17] Change api structure --- .../assertions/jest-like/api/identical-to.ts | 18 +++++++++ source/lib/assertions/jest-like/api/index.ts | 11 ++++++ .../jest-like/api/no-identical-to.ts | 18 +++++++++ source/lib/assertions/jest-like/assert.ts | 39 +++++-------------- source/lib/assertions/jest-like/util.ts | 6 +-- 5 files changed, 59 insertions(+), 33 deletions(-) create mode 100644 source/lib/assertions/jest-like/api/identical-to.ts create mode 100644 source/lib/assertions/jest-like/api/index.ts create mode 100644 source/lib/assertions/jest-like/api/no-identical-to.ts diff --git a/source/lib/assertions/jest-like/api/identical-to.ts b/source/lib/assertions/jest-like/api/identical-to.ts new file mode 100644 index 00000000..00ad738a --- /dev/null +++ b/source/lib/assertions/jest-like/api/identical-to.ts @@ -0,0 +1,18 @@ +/* eslint-disable @typescript-eslint/no-empty-function */ +/* eslint-disable @typescript-eslint/no-unused-vars */ + +/** + * Test that the type provided in the first generic parameter is identical to expected type. + * + * @template TargetType - The expected type that will be compared with another type. + */ +export function identicalTo(): void; + +/** + * Test that the type provided in the first argument is identical to expected type. + * + * @param targetValue - The expected value whose type will be compared with another type. + */ +export function identicalTo(targetValue: TargetValueType): void; + +export function identicalTo(): void { } diff --git a/source/lib/assertions/jest-like/api/index.ts b/source/lib/assertions/jest-like/api/index.ts new file mode 100644 index 00000000..a0f62603 --- /dev/null +++ b/source/lib/assertions/jest-like/api/index.ts @@ -0,0 +1,11 @@ +import {identicalTo} from './identical-to'; +import {notIdenticalTo} from './no-identical-to'; + +export const api = { + identicalTo, + not: { + identicalTo: notIdenticalTo, + }, +}; + +export type AssertTypeAPI = typeof api; diff --git a/source/lib/assertions/jest-like/api/no-identical-to.ts b/source/lib/assertions/jest-like/api/no-identical-to.ts new file mode 100644 index 00000000..e031cd91 --- /dev/null +++ b/source/lib/assertions/jest-like/api/no-identical-to.ts @@ -0,0 +1,18 @@ +/* eslint-disable @typescript-eslint/no-empty-function */ +/* eslint-disable @typescript-eslint/no-unused-vars */ + +/** + * Test that the type provided in the first generic parameter is not identical to expected type. + * + * @template TargetType - The expected type that will be compared with another type. + */ +export function notIdenticalTo(): void; + +/** + * Test that the type provided in the first argument is not identical to expected type. + * + * @param targetValue - The expected value whose type will be compared with another type. + */ +export function notIdenticalTo(targetValue: TargetValueType): void; + +export function notIdenticalTo(): void { } diff --git a/source/lib/assertions/jest-like/assert.ts b/source/lib/assertions/jest-like/assert.ts index 72072af9..a43cd602 100644 --- a/source/lib/assertions/jest-like/assert.ts +++ b/source/lib/assertions/jest-like/assert.ts @@ -1,41 +1,20 @@ -/* eslint-disable @typescript-eslint/no-empty-function */ /* eslint-disable @typescript-eslint/no-unused-vars */ +import {api, AssertTypeAPI} from './api'; /** - * Test that target type is identical to expected type. + * Create a type assertion holder from the type provided in the first generic parameter. * - * @template TargetType - The expected type that will be compared with another type. - * @param targetValue - The expected value whose type will be compared with another type. + * @template ExpectedType - The expected type that will be compared with another type. */ -// @ts-expect-error no-unused-vars -function identicalTo(targetValue?: TargetType) {} +export function assertType(): AssertTypeAPI; /** - * Test that target type is not identical to expected type. + * Create a type assertion holder from the type provided in the first argument. * - * @template TargetType - The expected type that will be compared with another type. - * @param targetValue - The expected value whose type will be compared with another type. - */ -// @ts-expect-error no-unused-vars -function notIdenticalTo(targetValue?: TargetType) {} - -// Construct the public API. -const assertTypeAPI = { - identicalTo, - not: { - identicalTo: notIdenticalTo, - }, -}; - -type AssertTypeAPI = typeof assertTypeAPI; - -/** - * Create a type assertion holder from the type provided in the first generic parameter or from the type of the first argument. - * - * @template ExpectedType - The expected type that will be compared with another type. * @param expectedValue - The expected value whose type will be compared with another type. */ -// @ts-expect-error no-unused-vars -export function assertType(expectedValue?: unknown): AssertTypeAPI { - return assertTypeAPI; +export function assertType(expectedValue: ExpectedValueType): AssertTypeAPI; + +export function assertType(): AssertTypeAPI { + return api; } diff --git a/source/lib/assertions/jest-like/util.ts b/source/lib/assertions/jest-like/util.ts index b5b35056..d55c7964 100644 --- a/source/lib/assertions/jest-like/util.ts +++ b/source/lib/assertions/jest-like/util.ts @@ -6,7 +6,7 @@ type Types = | {type: Type; argument: Node; diagnostic?: never} | {diagnostic: Diagnostic; type?: never; argument?: never}; -export function getTypes(node: CallExpression, typeChecker: TypeChecker): Types { +export function getTypes(node: CallExpression, checker: TypeChecker): Types { let type: Type | undefined; let value: Type | undefined; @@ -14,11 +14,11 @@ export function getTypes(node: CallExpression, typeChecker: TypeChecker): Types const valueArgument = node.arguments[0]; if (typeArgument) { - type = typeChecker.getTypeFromTypeNode(typeArgument); + type = checker.getTypeFromTypeNode(typeArgument); } if (valueArgument) { - value = typeChecker.getTypeAtLocation(valueArgument); + value = checker.getTypeAtLocation(valueArgument); } if (type && value) { From 55f340f37e0732fd1e02624bc66a8c01da2484b8 Mon Sep 17 00:00:00 2001 From: skarab42 Date: Sat, 22 Oct 2022 14:34:02 +0200 Subject: [PATCH 05/17] Refactor test structure --- .../{identicality => identical-to}/index.d.ts | 0 .../{identicality => identical-to}/index.js | 0 .../identical-to/index.test-d.ts | 16 ++++++++++ .../package.json | 0 .../jest-like-api/not-identical-to/index.d.ts | 0 .../jest-like-api/not-identical-to/index.js | 0 .../index.test-d.ts | 10 ------ .../not-identical-to/package.json | 3 ++ source/test/jest-like-api.ts | 31 ++++++++++++------- 9 files changed, 38 insertions(+), 22 deletions(-) rename source/test/fixtures/jest-like-api/{identicality => identical-to}/index.d.ts (100%) rename source/test/fixtures/jest-like-api/{identicality => identical-to}/index.js (100%) create mode 100644 source/test/fixtures/jest-like-api/identical-to/index.test-d.ts rename source/test/fixtures/jest-like-api/{identicality => identical-to}/package.json (100%) create mode 100644 source/test/fixtures/jest-like-api/not-identical-to/index.d.ts create mode 100644 source/test/fixtures/jest-like-api/not-identical-to/index.js rename source/test/fixtures/jest-like-api/{identicality => not-identical-to}/index.test-d.ts (59%) create mode 100644 source/test/fixtures/jest-like-api/not-identical-to/package.json diff --git a/source/test/fixtures/jest-like-api/identicality/index.d.ts b/source/test/fixtures/jest-like-api/identical-to/index.d.ts similarity index 100% rename from source/test/fixtures/jest-like-api/identicality/index.d.ts rename to source/test/fixtures/jest-like-api/identical-to/index.d.ts diff --git a/source/test/fixtures/jest-like-api/identicality/index.js b/source/test/fixtures/jest-like-api/identical-to/index.js similarity index 100% rename from source/test/fixtures/jest-like-api/identicality/index.js rename to source/test/fixtures/jest-like-api/identical-to/index.js diff --git a/source/test/fixtures/jest-like-api/identical-to/index.test-d.ts b/source/test/fixtures/jest-like-api/identical-to/index.test-d.ts new file mode 100644 index 00000000..9fb99577 --- /dev/null +++ b/source/test/fixtures/jest-like-api/identical-to/index.test-d.ts @@ -0,0 +1,16 @@ +import {assertType} from '../../../..'; + +declare const fooString: string; +declare const fooNumber: number; + +// Should pass +assertType().identicalTo(); +assertType().identicalTo(fooString); +assertType(fooString).identicalTo(); +assertType(fooString).identicalTo(fooString); + +// Shoul fail +assertType().identicalTo(); +assertType().identicalTo(fooString); +assertType(fooString).identicalTo(); +assertType(fooString).identicalTo(fooNumber); diff --git a/source/test/fixtures/jest-like-api/identicality/package.json b/source/test/fixtures/jest-like-api/identical-to/package.json similarity index 100% rename from source/test/fixtures/jest-like-api/identicality/package.json rename to source/test/fixtures/jest-like-api/identical-to/package.json diff --git a/source/test/fixtures/jest-like-api/not-identical-to/index.d.ts b/source/test/fixtures/jest-like-api/not-identical-to/index.d.ts new file mode 100644 index 00000000..e69de29b diff --git a/source/test/fixtures/jest-like-api/not-identical-to/index.js b/source/test/fixtures/jest-like-api/not-identical-to/index.js new file mode 100644 index 00000000..e69de29b diff --git a/source/test/fixtures/jest-like-api/identicality/index.test-d.ts b/source/test/fixtures/jest-like-api/not-identical-to/index.test-d.ts similarity index 59% rename from source/test/fixtures/jest-like-api/identicality/index.test-d.ts rename to source/test/fixtures/jest-like-api/not-identical-to/index.test-d.ts index e18dd224..84186c3a 100644 --- a/source/test/fixtures/jest-like-api/identicality/index.test-d.ts +++ b/source/test/fixtures/jest-like-api/not-identical-to/index.test-d.ts @@ -4,22 +4,12 @@ declare const fooString: string; declare const fooNumber: number; // Should pass -assertType().identicalTo(); -assertType().identicalTo(fooString); -assertType(fooString).identicalTo(); -assertType(fooString).identicalTo(fooString); - assertType().not.identicalTo(); assertType().not.identicalTo(fooString); assertType(fooString).not.identicalTo(); assertType(fooString).not.identicalTo(fooNumber); // Shoul fail -assertType().identicalTo(); -assertType().identicalTo(fooString); -assertType(fooString).identicalTo(); -assertType(fooString).identicalTo(fooNumber); - assertType().not.identicalTo(); assertType().not.identicalTo(fooString); assertType(fooString).not.identicalTo(); diff --git a/source/test/fixtures/jest-like-api/not-identical-to/package.json b/source/test/fixtures/jest-like-api/not-identical-to/package.json new file mode 100644 index 00000000..de6dc1db --- /dev/null +++ b/source/test/fixtures/jest-like-api/not-identical-to/package.json @@ -0,0 +1,3 @@ +{ + "name": "foo" +} diff --git a/source/test/jest-like-api.ts b/source/test/jest-like-api.ts index 97cbdb2a..7b22cde1 100644 --- a/source/test/jest-like-api.ts +++ b/source/test/jest-like-api.ts @@ -22,23 +22,30 @@ test('jest like API parser error', async t => { ]); }); -test('jest like API identicality', async t => { - const diagnostics = await tsd({cwd: path.join(__dirname, 'fixtures/jest-like-api/identicality')}); +test('identical-to', async t => { + const diagnostics = await tsd({cwd: path.join(__dirname, 'fixtures/jest-like-api/identical-to')}); verify(t, diagnostics, [ - [18, 0, 'error', 'Parameter type `string` is not identical to argument type `number`.'], - [19, 0, 'error', 'Parameter type `number` is not identical to argument type `string`.'], - [20, 0, 'error', 'Parameter type `string` is not identical to argument type `number`.'], - [21, 0, 'error', 'Parameter type `string` is not identical to argument type `number`.'], - [23, 0, 'error', 'Parameter type `string` is identical to argument type `string`.'], - [24, 0, 'error', 'Parameter type `string` is identical to argument type `string`.'], - [25, 0, 'error', 'Parameter type `string` is identical to argument type `string`.'], - [26, 0, 'error', 'Parameter type `string` is identical to argument type `string`.'], + [13, 0, 'error', 'Parameter type `string` is not identical to argument type `number`.'], + [14, 0, 'error', 'Parameter type `number` is not identical to argument type `string`.'], + [15, 0, 'error', 'Parameter type `string` is not identical to argument type `number`.'], + [16, 0, 'error', 'Parameter type `string` is not identical to argument type `number`.'], ]); }); -// Debug +test('not-identical-to', async t => { + const diagnostics = await tsd({cwd: path.join(__dirname, 'fixtures/jest-like-api/not-identical-to')}); + + verify(t, diagnostics, [ + [13, 0, 'error', 'Parameter type `string` is identical to argument type `string`.'], + [14, 0, 'error', 'Parameter type `string` is identical to argument type `string`.'], + [15, 0, 'error', 'Parameter type `string` is identical to argument type `string`.'], + [16, 0, 'error', 'Parameter type `string` is identical to argument type `string`.'], + ]); +}); + +// // Debug // test('debug', async () => { -// const diagnostics = await tsd({cwd: path.join(__dirname, 'fixtures/jest-like-api')}); +// const diagnostics = await tsd({cwd: path.join(__dirname, 'fixtures/jest-like-api/identicality')}); // console.log(diagnostics); // }); From d6f728f13b838dccf74433066034c39b450189b4 Mon Sep 17 00:00:00 2001 From: skarab42 Date: Sat, 22 Oct 2022 14:41:24 +0200 Subject: [PATCH 06/17] Add test for generic parameter inference #142 --- .../jest-like-api/identical-to/index.test-d.ts | 11 +++++++++++ .../jest-like-api/not-identical-to/index.test-d.ts | 11 +++++++++++ source/test/jest-like-api.ts | 4 ++++ 3 files changed, 26 insertions(+) diff --git a/source/test/fixtures/jest-like-api/identical-to/index.test-d.ts b/source/test/fixtures/jest-like-api/identical-to/index.test-d.ts index 9fb99577..023d95de 100644 --- a/source/test/fixtures/jest-like-api/identical-to/index.test-d.ts +++ b/source/test/fixtures/jest-like-api/identical-to/index.test-d.ts @@ -14,3 +14,14 @@ assertType().identicalTo(); assertType().identicalTo(fooString); assertType(fooString).identicalTo(); assertType(fooString).identicalTo(fooNumber); + +// Should handle generic, see https://github.com/SamVerschueren/tsd/issues/142 +declare const inferrable: () => T; + +// Should pass +assertType<'foo'>().identicalTo(inferrable()); +assertType(inferrable()).identicalTo('foo'); + +// Should fail +assertType().identicalTo(inferrable()); +assertType(inferrable()).identicalTo(fooString); diff --git a/source/test/fixtures/jest-like-api/not-identical-to/index.test-d.ts b/source/test/fixtures/jest-like-api/not-identical-to/index.test-d.ts index 84186c3a..74fd4756 100644 --- a/source/test/fixtures/jest-like-api/not-identical-to/index.test-d.ts +++ b/source/test/fixtures/jest-like-api/not-identical-to/index.test-d.ts @@ -14,3 +14,14 @@ assertType().not.identicalTo(); assertType().not.identicalTo(fooString); assertType(fooString).not.identicalTo(); assertType(fooString).not.identicalTo(fooString); + +// Should handle generic, see https://github.com/SamVerschueren/tsd/issues/142 +declare const inferrable: () => T; + +// Should pass +assertType().not.identicalTo(inferrable()); +assertType(inferrable()).not.identicalTo(fooString); + +// Should fail +assertType<'foo'>().not.identicalTo(inferrable()); +assertType(inferrable()).not.identicalTo('foo'); diff --git a/source/test/jest-like-api.ts b/source/test/jest-like-api.ts index 7b22cde1..df28db53 100644 --- a/source/test/jest-like-api.ts +++ b/source/test/jest-like-api.ts @@ -30,6 +30,8 @@ test('identical-to', async t => { [14, 0, 'error', 'Parameter type `number` is not identical to argument type `string`.'], [15, 0, 'error', 'Parameter type `string` is not identical to argument type `number`.'], [16, 0, 'error', 'Parameter type `string` is not identical to argument type `number`.'], + [26, 0, 'error', 'Parameter type `string` is not identical to argument type `"foo"`.'], + [27, 0, 'error', 'Parameter type `"foo"` is not identical to argument type `string`.'], ]); }); @@ -41,6 +43,8 @@ test('not-identical-to', async t => { [14, 0, 'error', 'Parameter type `string` is identical to argument type `string`.'], [15, 0, 'error', 'Parameter type `string` is identical to argument type `string`.'], [16, 0, 'error', 'Parameter type `string` is identical to argument type `string`.'], + [26, 0, 'error', 'Parameter type `"foo"` is identical to argument type `"foo"`.'], + [27, 0, 'error', 'Parameter type `"foo"` is identical to argument type `"foo"`.'], ]); }); From 1881e97e0719b4a4fab7754254c29a43de4fa81a Mon Sep 17 00:00:00 2001 From: skarab42 Date: Sat, 22 Oct 2022 14:59:50 +0200 Subject: [PATCH 07/17] Change diagnostic messages --- .../jest-like/handlers/is-identical.ts | 9 +------ .../jest-like/handlers/is-not-identical.ts | 9 +------ source/test/jest-like-api.ts | 24 +++++++++---------- 3 files changed, 14 insertions(+), 28 deletions(-) diff --git a/source/lib/assertions/jest-like/handlers/is-identical.ts b/source/lib/assertions/jest-like/handlers/is-identical.ts index 0b2a352e..93afa507 100644 --- a/source/lib/assertions/jest-like/handlers/is-identical.ts +++ b/source/lib/assertions/jest-like/handlers/is-identical.ts @@ -4,13 +4,6 @@ import {makeDiagnostic} from '../../../utils'; import {JestLikeAssertionNodes} from '..'; import {getTypes} from '../util'; -/** - * Asserts that the argument of the assertion is not identical 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 isIdentical = (checker: TypeChecker, nodes: JestLikeAssertionNodes): Diagnostic[] => { const diagnostics: Diagnostic[] = []; @@ -36,7 +29,7 @@ export const isIdentical = (checker: TypeChecker, nodes: JestLikeAssertionNodes) } if (!checker.isTypeIdenticalTo(expected.type, target.type)) { - diagnostics.push(makeDiagnostic(expectedNode, `Parameter type \`${checker.typeToString(expected.type)}\` is not identical to argument type \`${checker.typeToString(target.type)}\`.`)); + diagnostics.push(makeDiagnostic(expectedNode, `Expected type \`${checker.typeToString(expected.type)}\` is not identical to type \`${checker.typeToString(target.type)}\`.`)); } } diff --git a/source/lib/assertions/jest-like/handlers/is-not-identical.ts b/source/lib/assertions/jest-like/handlers/is-not-identical.ts index 5abad176..fa5d01a6 100644 --- a/source/lib/assertions/jest-like/handlers/is-not-identical.ts +++ b/source/lib/assertions/jest-like/handlers/is-not-identical.ts @@ -4,13 +4,6 @@ import {makeDiagnostic} from '../../../utils'; import {JestLikeAssertionNodes} from '..'; import {getTypes} from '../util'; -/** - * Asserts that the argument of the assertion is identical 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 isNotIdentical = (checker: TypeChecker, nodes: JestLikeAssertionNodes): Diagnostic[] => { const diagnostics: Diagnostic[] = []; @@ -36,7 +29,7 @@ export const isNotIdentical = (checker: TypeChecker, nodes: JestLikeAssertionNod } if (checker.isTypeIdenticalTo(expected.type, target.type)) { - diagnostics.push(makeDiagnostic(expectedNode, `Parameter type \`${checker.typeToString(expected.type)}\` is identical to argument type \`${checker.typeToString(target.type)}\`.`)); + diagnostics.push(makeDiagnostic(expectedNode, `Expected type \`${checker.typeToString(expected.type)}\` is identical to type \`${checker.typeToString(target.type)}\`.`)); } } diff --git a/source/test/jest-like-api.ts b/source/test/jest-like-api.ts index df28db53..e1dfea47 100644 --- a/source/test/jest-like-api.ts +++ b/source/test/jest-like-api.ts @@ -26,12 +26,12 @@ test('identical-to', async t => { const diagnostics = await tsd({cwd: path.join(__dirname, 'fixtures/jest-like-api/identical-to')}); verify(t, diagnostics, [ - [13, 0, 'error', 'Parameter type `string` is not identical to argument type `number`.'], - [14, 0, 'error', 'Parameter type `number` is not identical to argument type `string`.'], - [15, 0, 'error', 'Parameter type `string` is not identical to argument type `number`.'], - [16, 0, 'error', 'Parameter type `string` is not identical to argument type `number`.'], - [26, 0, 'error', 'Parameter type `string` is not identical to argument type `"foo"`.'], - [27, 0, 'error', 'Parameter type `"foo"` is not identical to argument type `string`.'], + [13, 0, 'error', 'Expected type `string` is not identical to type `number`.'], + [14, 0, 'error', 'Expected type `number` is not identical to type `string`.'], + [15, 0, 'error', 'Expected type `string` is not identical to type `number`.'], + [16, 0, 'error', 'Expected type `string` is not identical to type `number`.'], + [26, 0, 'error', 'Expected type `string` is not identical to type `"foo"`.'], + [27, 0, 'error', 'Expected type `"foo"` is not identical to type `string`.'], ]); }); @@ -39,12 +39,12 @@ test('not-identical-to', async t => { const diagnostics = await tsd({cwd: path.join(__dirname, 'fixtures/jest-like-api/not-identical-to')}); verify(t, diagnostics, [ - [13, 0, 'error', 'Parameter type `string` is identical to argument type `string`.'], - [14, 0, 'error', 'Parameter type `string` is identical to argument type `string`.'], - [15, 0, 'error', 'Parameter type `string` is identical to argument type `string`.'], - [16, 0, 'error', 'Parameter type `string` is identical to argument type `string`.'], - [26, 0, 'error', 'Parameter type `"foo"` is identical to argument type `"foo"`.'], - [27, 0, 'error', 'Parameter type `"foo"` is identical to argument type `"foo"`.'], + [13, 0, 'error', 'Expected type `string` is identical to type `string`.'], + [14, 0, 'error', 'Expected type `string` is identical to type `string`.'], + [15, 0, 'error', 'Expected type `string` is identical to type `string`.'], + [16, 0, 'error', 'Expected type `string` is identical to type `string`.'], + [26, 0, 'error', 'Expected type `"foo"` is identical to type `"foo"`.'], + [27, 0, 'error', 'Expected type `"foo"` is identical to type `"foo"`.'], ]); }); From b8561be6d5ae3fef7b0f6dd31a283d231e89b7b1 Mon Sep 17 00:00:00 2001 From: skarab42 Date: Sat, 22 Oct 2022 16:04:46 +0200 Subject: [PATCH 08/17] Add more tests --- .../fixtures/jest-like-api/identical-to/index.test-d.ts | 6 ++++++ .../fixtures/jest-like-api/not-identical-to/index.test-d.ts | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/source/test/fixtures/jest-like-api/identical-to/index.test-d.ts b/source/test/fixtures/jest-like-api/identical-to/index.test-d.ts index 023d95de..d51d646d 100644 --- a/source/test/fixtures/jest-like-api/identical-to/index.test-d.ts +++ b/source/test/fixtures/jest-like-api/identical-to/index.test-d.ts @@ -15,6 +15,12 @@ assertType().identicalTo(fooString); assertType(fooString).identicalTo(); assertType(fooString).identicalTo(fooNumber); +// Should fail with assignable type +assertType<'foo'>().identicalTo(); +assertType<'foo'>().identicalTo(fooString); +assertType('foo').identicalTo(); +assertType('foo').identicalTo(fooString); + // Should handle generic, see https://github.com/SamVerschueren/tsd/issues/142 declare const inferrable: () => T; diff --git a/source/test/fixtures/jest-like-api/not-identical-to/index.test-d.ts b/source/test/fixtures/jest-like-api/not-identical-to/index.test-d.ts index 74fd4756..8de15f6f 100644 --- a/source/test/fixtures/jest-like-api/not-identical-to/index.test-d.ts +++ b/source/test/fixtures/jest-like-api/not-identical-to/index.test-d.ts @@ -15,6 +15,12 @@ assertType().not.identicalTo(fooString); assertType(fooString).not.identicalTo(); assertType(fooString).not.identicalTo(fooString); +// Should pass with assignable type +assertType<'foo'>().not.identicalTo(); +assertType<'foo'>().not.identicalTo(fooString); +assertType('foo').not.identicalTo(); +assertType('foo').not.identicalTo(fooString); + // Should handle generic, see https://github.com/SamVerschueren/tsd/issues/142 declare const inferrable: () => T; From d214cb1f2495f70c2aa4ddf8d0ed21d781fd988e Mon Sep 17 00:00:00 2001 From: skarab42 Date: Sat, 22 Oct 2022 16:06:51 +0200 Subject: [PATCH 09/17] Fix filename typo --- source/lib/assertions/jest-like/api/index.ts | 2 +- .../jest-like/api/{no-identical-to.ts => not-identical-to.ts} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename source/lib/assertions/jest-like/api/{no-identical-to.ts => not-identical-to.ts} (100%) diff --git a/source/lib/assertions/jest-like/api/index.ts b/source/lib/assertions/jest-like/api/index.ts index a0f62603..0f02afe2 100644 --- a/source/lib/assertions/jest-like/api/index.ts +++ b/source/lib/assertions/jest-like/api/index.ts @@ -1,5 +1,5 @@ import {identicalTo} from './identical-to'; -import {notIdenticalTo} from './no-identical-to'; +import {notIdenticalTo} from './not-identical-to'; export const api = { identicalTo, diff --git a/source/lib/assertions/jest-like/api/no-identical-to.ts b/source/lib/assertions/jest-like/api/not-identical-to.ts similarity index 100% rename from source/lib/assertions/jest-like/api/no-identical-to.ts rename to source/lib/assertions/jest-like/api/not-identical-to.ts From e0538bb16559678bc1d04c04ae749afc8d5dd2f3 Mon Sep 17 00:00:00 2001 From: skarab42 Date: Sat, 22 Oct 2022 16:48:24 +0200 Subject: [PATCH 10/17] Add assignability handlers --- .../assertions/jest-like/api/assignable-to.ts | 18 ++++++++ source/lib/assertions/jest-like/api/index.ts | 5 ++ .../jest-like/api/not-assignable-to.ts | 18 ++++++++ .../assertions/jest-like/handlers/index.ts | 3 ++ .../jest-like/handlers/is-assignable.ts | 37 +++++++++++++++ .../jest-like/handlers/is-not-assignable.ts | 37 +++++++++++++++ source/lib/assertions/jest-like/index.ts | 13 +++++- .../jest-like-api/assignable-to/index.d.ts | 0 .../jest-like-api/assignable-to/index.js | 0 .../assignable-to/index.test-d.ts | 39 ++++++++++++++++ .../jest-like-api/assignable-to/package.json | 3 ++ .../not-assignable-to/index.d.ts | 0 .../jest-like-api/not-assignable-to/index.js | 0 .../not-assignable-to/index.test-d.ts | 39 ++++++++++++++++ .../not-assignable-to/package.json | 3 ++ source/test/jest-like-api.ts | 46 +++++++++++++++++-- 16 files changed, 256 insertions(+), 5 deletions(-) create mode 100644 source/lib/assertions/jest-like/api/assignable-to.ts create mode 100644 source/lib/assertions/jest-like/api/not-assignable-to.ts create mode 100644 source/lib/assertions/jest-like/handlers/is-assignable.ts create mode 100644 source/lib/assertions/jest-like/handlers/is-not-assignable.ts create mode 100644 source/test/fixtures/jest-like-api/assignable-to/index.d.ts create mode 100644 source/test/fixtures/jest-like-api/assignable-to/index.js create mode 100644 source/test/fixtures/jest-like-api/assignable-to/index.test-d.ts create mode 100644 source/test/fixtures/jest-like-api/assignable-to/package.json create mode 100644 source/test/fixtures/jest-like-api/not-assignable-to/index.d.ts create mode 100644 source/test/fixtures/jest-like-api/not-assignable-to/index.js create mode 100644 source/test/fixtures/jest-like-api/not-assignable-to/index.test-d.ts create mode 100644 source/test/fixtures/jest-like-api/not-assignable-to/package.json diff --git a/source/lib/assertions/jest-like/api/assignable-to.ts b/source/lib/assertions/jest-like/api/assignable-to.ts new file mode 100644 index 00000000..b51f32f3 --- /dev/null +++ b/source/lib/assertions/jest-like/api/assignable-to.ts @@ -0,0 +1,18 @@ +/* eslint-disable @typescript-eslint/no-empty-function */ +/* eslint-disable @typescript-eslint/no-unused-vars */ + +/** + * Test that the type provided in the first generic parameter is identical to expected type. + * + * @template TargetType - The expected type that will be compared with another type. + */ +export function assignableTo(): void; + +/** + * Test that the type provided in the first argument is identical to expected type. + * + * @param targetValue - The expected value whose type will be compared with another type. + */ +export function assignableTo(targetValue: TargetValueType): void; + +export function assignableTo(): void { } diff --git a/source/lib/assertions/jest-like/api/index.ts b/source/lib/assertions/jest-like/api/index.ts index 0f02afe2..64b66c2d 100644 --- a/source/lib/assertions/jest-like/api/index.ts +++ b/source/lib/assertions/jest-like/api/index.ts @@ -1,9 +1,14 @@ +import {assignableTo} from './assignable-to'; import {identicalTo} from './identical-to'; + +import {notAssignableTo} from './not-assignable-to'; import {notIdenticalTo} from './not-identical-to'; export const api = { + assignableTo, identicalTo, not: { + assignableTo: notAssignableTo, identicalTo: notIdenticalTo, }, }; diff --git a/source/lib/assertions/jest-like/api/not-assignable-to.ts b/source/lib/assertions/jest-like/api/not-assignable-to.ts new file mode 100644 index 00000000..afa249d4 --- /dev/null +++ b/source/lib/assertions/jest-like/api/not-assignable-to.ts @@ -0,0 +1,18 @@ +/* eslint-disable @typescript-eslint/no-empty-function */ +/* eslint-disable @typescript-eslint/no-unused-vars */ + +/** + * Test that the type provided in the first generic parameter is not identical to expected type. + * + * @template TargetType - The expected type that will be compared with another type. + */ +export function notAssignableTo(): void; + +/** + * Test that the type provided in the first argument is not identical to expected type. + * + * @param targetValue - The expected value whose type will be compared with another type. + */ +export function notAssignableTo(targetValue: TargetValueType): void; + +export function notAssignableTo(): void { } diff --git a/source/lib/assertions/jest-like/handlers/index.ts b/source/lib/assertions/jest-like/handlers/index.ts index a8762e1a..643317bf 100644 --- a/source/lib/assertions/jest-like/handlers/index.ts +++ b/source/lib/assertions/jest-like/handlers/index.ts @@ -1,2 +1,5 @@ +export {isAssignable} from './is-assignable'; export {isIdentical} from './is-identical'; + +export {isNotAssignable} from './is-not-assignable'; export {isNotIdentical} from './is-not-identical'; diff --git a/source/lib/assertions/jest-like/handlers/is-assignable.ts b/source/lib/assertions/jest-like/handlers/is-assignable.ts new file mode 100644 index 00000000..836d9136 --- /dev/null +++ b/source/lib/assertions/jest-like/handlers/is-assignable.ts @@ -0,0 +1,37 @@ +import {TypeChecker} from '@tsd/typescript'; +import {Diagnostic} from '../../../interfaces'; +import {makeDiagnostic} from '../../../utils'; +import {JestLikeAssertionNodes} from '..'; +import {getTypes} from '../util'; + +export const isAssignable = (checker: TypeChecker, nodes: JestLikeAssertionNodes): Diagnostic[] => { + const diagnostics: Diagnostic[] = []; + + if (!nodes) { + return diagnostics; + } + + for (const node of nodes) { + const [expectedNode, targetNode] = node; + + const expected = getTypes(expectedNode, checker); + + if (expected.diagnostic) { + diagnostics.push(expected.diagnostic); + continue; + } + + const target = getTypes(targetNode, checker); + + if (target.diagnostic) { + diagnostics.push(target.diagnostic); + continue; + } + + if (!checker.isTypeAssignableTo(expected.type, target.type)) { + diagnostics.push(makeDiagnostic(expectedNode, `Expected type \`${checker.typeToString(expected.type)}\` is not assignable to type \`${checker.typeToString(target.type)}\`.`)); + } + } + + return diagnostics; +}; diff --git a/source/lib/assertions/jest-like/handlers/is-not-assignable.ts b/source/lib/assertions/jest-like/handlers/is-not-assignable.ts new file mode 100644 index 00000000..d5d6d976 --- /dev/null +++ b/source/lib/assertions/jest-like/handlers/is-not-assignable.ts @@ -0,0 +1,37 @@ +import {TypeChecker} from '@tsd/typescript'; +import {Diagnostic} from '../../../interfaces'; +import {makeDiagnostic} from '../../../utils'; +import {JestLikeAssertionNodes} from '..'; +import {getTypes} from '../util'; + +export const isNotAssignable = (checker: TypeChecker, nodes: JestLikeAssertionNodes): Diagnostic[] => { + const diagnostics: Diagnostic[] = []; + + if (!nodes) { + return diagnostics; + } + + for (const node of nodes) { + const [expectedNode, targetNode] = node; + + const expected = getTypes(expectedNode, checker); + + if (expected.diagnostic) { + diagnostics.push(expected.diagnostic); + continue; + } + + const target = getTypes(targetNode, checker); + + if (target.diagnostic) { + diagnostics.push(target.diagnostic); + continue; + } + + if (checker.isTypeAssignableTo(expected.type, target.type)) { + diagnostics.push(makeDiagnostic(expectedNode, `Expected type \`${checker.typeToString(expected.type)}\` is assignable to type \`${checker.typeToString(target.type)}\`.`)); + } + } + + return diagnostics; +}; diff --git a/source/lib/assertions/jest-like/index.ts b/source/lib/assertions/jest-like/index.ts index a27e6231..59415f6f 100644 --- a/source/lib/assertions/jest-like/index.ts +++ b/source/lib/assertions/jest-like/index.ts @@ -1,6 +1,11 @@ import {CallExpression, TypeChecker} from '@tsd/typescript'; import {Diagnostic} from '../../interfaces'; -import {isIdentical, isNotIdentical} from '../jest-like/handlers'; +import { + isAssignable, + isIdentical, + isNotAssignable, + isNotIdentical +} from '../jest-like/handlers'; export type JestLikeAssertionNodes = Set<[CallExpression, CallExpression]>; @@ -18,13 +23,19 @@ export type JestLikeAssertionHandlers = Map; export type JestLikeAssertions = Map; export enum JestLikeAssertion { + ASSIGNABLE_TO = 'assignableTo', IDENTICAL_TO = 'identicalTo', + + NOT_ASSIGNABLE_TO = 'notAssignableTo', NOT_IDENTICAL_TO = 'notIdenticalTo', } // List of diagnostic handlers attached to the assertion const assertionHandlers: JestLikeAssertionHandlers = new Map([ + [JestLikeAssertion.ASSIGNABLE_TO, isAssignable], [JestLikeAssertion.IDENTICAL_TO, isIdentical], + + [JestLikeAssertion.NOT_ASSIGNABLE_TO, isNotAssignable], [JestLikeAssertion.NOT_IDENTICAL_TO, isNotIdentical], ]); diff --git a/source/test/fixtures/jest-like-api/assignable-to/index.d.ts b/source/test/fixtures/jest-like-api/assignable-to/index.d.ts new file mode 100644 index 00000000..e69de29b diff --git a/source/test/fixtures/jest-like-api/assignable-to/index.js b/source/test/fixtures/jest-like-api/assignable-to/index.js new file mode 100644 index 00000000..e69de29b diff --git a/source/test/fixtures/jest-like-api/assignable-to/index.test-d.ts b/source/test/fixtures/jest-like-api/assignable-to/index.test-d.ts new file mode 100644 index 00000000..7d3534ed --- /dev/null +++ b/source/test/fixtures/jest-like-api/assignable-to/index.test-d.ts @@ -0,0 +1,39 @@ +import {assertType} from '../../../..'; + +declare const fooString: string; +declare const fooNumber: number; + +// Should pass +assertType().assignableTo(); +assertType().assignableTo(fooString); +assertType(fooString).assignableTo(); +assertType(fooString).assignableTo(fooString); + +// Shoul fail +assertType().assignableTo(); +assertType().assignableTo(fooString); +assertType(fooString).assignableTo(); +assertType(fooString).assignableTo(fooNumber); + +// Should pass with assignable type +assertType<'foo'>().assignableTo(); +assertType<'foo'>().assignableTo(fooString); +assertType('foo').assignableTo(); +assertType('foo').assignableTo(fooString); + +// Should fail with reversed order (assignable type) +assertType().assignableTo<'foo'>(); +assertType().assignableTo('foo'); +assertType(fooString).assignableTo<'foo'>(); +assertType(fooString).assignableTo('foo'); + +// Should handle generic, see https://github.com/SamVerschueren/tsd/issues/142 +declare const inferrable: () => T; + +// Should pass +assertType<'foo'>().assignableTo(inferrable()); +assertType(inferrable()).assignableTo('foo'); + +// Should fail +assertType().assignableTo(inferrable()); +assertType(inferrable()).assignableTo(fooNumber); diff --git a/source/test/fixtures/jest-like-api/assignable-to/package.json b/source/test/fixtures/jest-like-api/assignable-to/package.json new file mode 100644 index 00000000..de6dc1db --- /dev/null +++ b/source/test/fixtures/jest-like-api/assignable-to/package.json @@ -0,0 +1,3 @@ +{ + "name": "foo" +} diff --git a/source/test/fixtures/jest-like-api/not-assignable-to/index.d.ts b/source/test/fixtures/jest-like-api/not-assignable-to/index.d.ts new file mode 100644 index 00000000..e69de29b diff --git a/source/test/fixtures/jest-like-api/not-assignable-to/index.js b/source/test/fixtures/jest-like-api/not-assignable-to/index.js new file mode 100644 index 00000000..e69de29b diff --git a/source/test/fixtures/jest-like-api/not-assignable-to/index.test-d.ts b/source/test/fixtures/jest-like-api/not-assignable-to/index.test-d.ts new file mode 100644 index 00000000..c4c71b54 --- /dev/null +++ b/source/test/fixtures/jest-like-api/not-assignable-to/index.test-d.ts @@ -0,0 +1,39 @@ +import {assertType} from '../../../..'; + +declare const fooString: string; +declare const fooNumber: number; + +// Should pass +assertType().not.assignableTo(); +assertType().not.assignableTo(fooString); +assertType(fooString).not.assignableTo(); +assertType(fooString).not.assignableTo(fooNumber); + +// Shoul fail +assertType().not.assignableTo(); +assertType().not.assignableTo(fooString); +assertType(fooString).not.assignableTo(); +assertType(fooString).not.assignableTo(fooString); + +// Should pass with assignable type +assertType().not.assignableTo<'foo'>(); +assertType().not.assignableTo('foo'); +assertType(fooString).not.assignableTo<'foo'>(); +assertType(fooString).not.assignableTo('foo'); + +// Should fail with reversed order (assignable type) +assertType<'foo'>().not.assignableTo(); +assertType<'foo'>().not.assignableTo(fooString); +assertType('foo').not.assignableTo(); +assertType('foo').not.assignableTo(fooString); + +// Should handle generic, see https://github.com/SamVerschueren/tsd/issues/142 +declare const inferrable: () => T; + +// Should pass +assertType().not.assignableTo(inferrable()); +assertType(inferrable()).not.assignableTo(fooNumber); + +// Should fail +assertType<'foo'>().not.assignableTo(inferrable()); +assertType(inferrable()).not.assignableTo('foo'); diff --git a/source/test/fixtures/jest-like-api/not-assignable-to/package.json b/source/test/fixtures/jest-like-api/not-assignable-to/package.json new file mode 100644 index 00000000..de6dc1db --- /dev/null +++ b/source/test/fixtures/jest-like-api/not-assignable-to/package.json @@ -0,0 +1,3 @@ +{ + "name": "foo" +} diff --git a/source/test/jest-like-api.ts b/source/test/jest-like-api.ts index e1dfea47..ab6ffc83 100644 --- a/source/test/jest-like-api.ts +++ b/source/test/jest-like-api.ts @@ -30,8 +30,12 @@ test('identical-to', async t => { [14, 0, 'error', 'Expected type `number` is not identical to type `string`.'], [15, 0, 'error', 'Expected type `string` is not identical to type `number`.'], [16, 0, 'error', 'Expected type `string` is not identical to type `number`.'], - [26, 0, 'error', 'Expected type `string` is not identical to type `"foo"`.'], - [27, 0, 'error', 'Expected type `"foo"` is not identical to type `string`.'], + [19, 0, 'error', 'Expected type `"foo"` is not identical to type `string`.'], + [20, 0, 'error', 'Expected type `"foo"` is not identical to type `string`.'], + [21, 0, 'error', 'Expected type `"foo"` is not identical to type `string`.'], + [22, 0, 'error', 'Expected type `"foo"` is not identical to type `string`.'], + [32, 0, 'error', 'Expected type `string` is not identical to type `"foo"`.'], + [33, 0, 'error', 'Expected type `"foo"` is not identical to type `string`.'], ]); }); @@ -43,8 +47,42 @@ test('not-identical-to', async t => { [14, 0, 'error', 'Expected type `string` is identical to type `string`.'], [15, 0, 'error', 'Expected type `string` is identical to type `string`.'], [16, 0, 'error', 'Expected type `string` is identical to type `string`.'], - [26, 0, 'error', 'Expected type `"foo"` is identical to type `"foo"`.'], - [27, 0, 'error', 'Expected type `"foo"` is identical to type `"foo"`.'], + [32, 0, 'error', 'Expected type `"foo"` is identical to type `"foo"`.'], + [33, 0, 'error', 'Expected type `"foo"` is identical to type `"foo"`.'], + ]); +}); + +test('assignable-to', async t => { + const diagnostics = await tsd({cwd: path.join(__dirname, 'fixtures/jest-like-api/assignable-to')}); + + verify(t, diagnostics, [ + [13, 0, 'error', 'Expected type `string` is not assignable to type `number`.'], + [14, 0, 'error', 'Expected type `number` is not assignable to type `string`.'], + [15, 0, 'error', 'Expected type `string` is not assignable to type `number`.'], + [16, 0, 'error', 'Expected type `string` is not assignable to type `number`.'], + [25, 0, 'error', 'Expected type `string` is not assignable to type `"foo"`.'], + [26, 0, 'error', 'Expected type `string` is not assignable to type `"foo"`.'], + [27, 0, 'error', 'Expected type `string` is not assignable to type `"foo"`.'], + [28, 0, 'error', 'Expected type `string` is not assignable to type `"foo"`.'], + [38, 0, 'error', 'Expected type `string` is not assignable to type `"foo"`.'], + [39, 0, 'error', 'Expected type `"foo"` is not assignable to type `number`.'], + ]); +}); + +test('not-assignable-to', async t => { + const diagnostics = await tsd({cwd: path.join(__dirname, 'fixtures/jest-like-api/not-assignable-to')}); + + verify(t, diagnostics, [ + [13, 0, 'error', 'Expected type `string` is assignable to type `string`.'], + [14, 0, 'error', 'Expected type `string` is assignable to type `string`.'], + [15, 0, 'error', 'Expected type `string` is assignable to type `string`.'], + [16, 0, 'error', 'Expected type `string` is assignable to type `string`.'], + [25, 0, 'error', 'Expected type `"foo"` is assignable to type `string`.'], + [26, 0, 'error', 'Expected type `"foo"` is assignable to type `string`.'], + [27, 0, 'error', 'Expected type `"foo"` is assignable to type `string`.'], + [28, 0, 'error', 'Expected type `"foo"` is assignable to type `string`.'], + [38, 0, 'error', 'Expected type `"foo"` is assignable to type `"foo"`.'], + [39, 0, 'error', 'Expected type `"foo"` is assignable to type `"foo"`.'], ]); }); From 0ee657fbebfc18fcfc824a25525b9bd41177987a Mon Sep 17 00:00:00 2001 From: skarab42 Date: Sun, 23 Oct 2022 08:24:23 +0200 Subject: [PATCH 11/17] Improve `identicalTo` error messages --- .../jest-like/handlers/is-identical.ts | 10 ++++++++++ source/test/jest-like-api.ts | 20 +++++++++---------- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/source/lib/assertions/jest-like/handlers/is-identical.ts b/source/lib/assertions/jest-like/handlers/is-identical.ts index 93afa507..f9fe33f6 100644 --- a/source/lib/assertions/jest-like/handlers/is-identical.ts +++ b/source/lib/assertions/jest-like/handlers/is-identical.ts @@ -28,6 +28,16 @@ export const isIdentical = (checker: TypeChecker, nodes: JestLikeAssertionNodes) continue; } + if (!checker.isTypeAssignableTo(expected.type, target.type)) { + diagnostics.push(makeDiagnostic(expectedNode, `Expected type \`${checker.typeToString(expected.type)}\` is declared too wide for type \`${checker.typeToString(target.type)}\`.`)); + continue; + } + + if (!checker.isTypeAssignableTo(target.type, expected.type)) { + diagnostics.push(makeDiagnostic(expectedNode, `Expected type \`${checker.typeToString(expected.type)}\` is declared too short for type \`${checker.typeToString(target.type)}\`.`)); + continue; + } + if (!checker.isTypeIdenticalTo(expected.type, target.type)) { diagnostics.push(makeDiagnostic(expectedNode, `Expected type \`${checker.typeToString(expected.type)}\` is not identical to type \`${checker.typeToString(target.type)}\`.`)); } diff --git a/source/test/jest-like-api.ts b/source/test/jest-like-api.ts index ab6ffc83..bba5a2ee 100644 --- a/source/test/jest-like-api.ts +++ b/source/test/jest-like-api.ts @@ -26,16 +26,16 @@ test('identical-to', async t => { const diagnostics = await tsd({cwd: path.join(__dirname, 'fixtures/jest-like-api/identical-to')}); verify(t, diagnostics, [ - [13, 0, 'error', 'Expected type `string` is not identical to type `number`.'], - [14, 0, 'error', 'Expected type `number` is not identical to type `string`.'], - [15, 0, 'error', 'Expected type `string` is not identical to type `number`.'], - [16, 0, 'error', 'Expected type `string` is not identical to type `number`.'], - [19, 0, 'error', 'Expected type `"foo"` is not identical to type `string`.'], - [20, 0, 'error', 'Expected type `"foo"` is not identical to type `string`.'], - [21, 0, 'error', 'Expected type `"foo"` is not identical to type `string`.'], - [22, 0, 'error', 'Expected type `"foo"` is not identical to type `string`.'], - [32, 0, 'error', 'Expected type `string` is not identical to type `"foo"`.'], - [33, 0, 'error', 'Expected type `"foo"` is not identical to type `string`.'], + [13, 0, 'error', 'Expected type `string` is declared too wide for type `number`.'], + [14, 0, 'error', 'Expected type `number` is declared too wide for type `string`.'], + [15, 0, 'error', 'Expected type `string` is declared too wide for type `number`.'], + [16, 0, 'error', 'Expected type `string` is declared too wide for type `number`.'], + [19, 0, 'error', 'Expected type `"foo"` is declared too short for type `string`.'], + [20, 0, 'error', 'Expected type `"foo"` is declared too short for type `string`.'], + [21, 0, 'error', 'Expected type `"foo"` is declared too short for type `string`.'], + [22, 0, 'error', 'Expected type `"foo"` is declared too short for type `string`.'], + [32, 0, 'error', 'Expected type `string` is declared too wide for type `"foo"`.'], + [33, 0, 'error', 'Expected type `"foo"` is declared too short for type `string`.'], ]); }); From c42918015e928b161326db10f6954a8ade94428e Mon Sep 17 00:00:00 2001 From: skarab42 Date: Sun, 23 Oct 2022 08:52:40 +0200 Subject: [PATCH 12/17] Docs typo --- source/lib/assertions/jest-like/api/assignable-to.ts | 10 +++++----- source/lib/assertions/jest-like/api/identical-to.ts | 10 +++++----- .../lib/assertions/jest-like/api/not-assignable-to.ts | 10 +++++----- .../lib/assertions/jest-like/api/not-identical-to.ts | 10 +++++----- source/lib/assertions/jest-like/assert.ts | 6 +++--- 5 files changed, 23 insertions(+), 23 deletions(-) diff --git a/source/lib/assertions/jest-like/api/assignable-to.ts b/source/lib/assertions/jest-like/api/assignable-to.ts index b51f32f3..5d836519 100644 --- a/source/lib/assertions/jest-like/api/assignable-to.ts +++ b/source/lib/assertions/jest-like/api/assignable-to.ts @@ -2,17 +2,17 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ /** - * Test that the type provided in the first generic parameter is identical to expected type. + * Test that the expected type is assignable to the type provided in the first generic parameter. * - * @template TargetType - The expected type that will be compared with another type. + * @template TargetType - The target type that will be compared with expected type. */ export function assignableTo(): void; /** - * Test that the type provided in the first argument is identical to expected type. + * Test that the expected type is assignable to the type of the expression provided in the first argument. * - * @param targetValue - The expected value whose type will be compared with another type. + * @param expression - An expression whose type will be compared with expected type. */ -export function assignableTo(targetValue: TargetValueType): void; +export function assignableTo(expression: ExpressionType): void; export function assignableTo(): void { } diff --git a/source/lib/assertions/jest-like/api/identical-to.ts b/source/lib/assertions/jest-like/api/identical-to.ts index 00ad738a..c690fcc2 100644 --- a/source/lib/assertions/jest-like/api/identical-to.ts +++ b/source/lib/assertions/jest-like/api/identical-to.ts @@ -2,17 +2,17 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ /** - * Test that the type provided in the first generic parameter is identical to expected type. + * Test that the expected type is identical to the type provided in the first generic parameter. * - * @template TargetType - The expected type that will be compared with another type. + * @template TargetType - The target type that will be compared with expected type. */ export function identicalTo(): void; /** - * Test that the type provided in the first argument is identical to expected type. + * Test that the expected type is identical to the type of the expression provided in the first argument. * - * @param targetValue - The expected value whose type will be compared with another type. + * @param expression - An expression whose type will be compared with expected type. */ -export function identicalTo(targetValue: TargetValueType): void; +export function identicalTo(expression: ExpressionType): void; export function identicalTo(): void { } diff --git a/source/lib/assertions/jest-like/api/not-assignable-to.ts b/source/lib/assertions/jest-like/api/not-assignable-to.ts index afa249d4..2729ae7d 100644 --- a/source/lib/assertions/jest-like/api/not-assignable-to.ts +++ b/source/lib/assertions/jest-like/api/not-assignable-to.ts @@ -2,17 +2,17 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ /** - * Test that the type provided in the first generic parameter is not identical to expected type. + * Test that the expected type is not assignable to the type provided in the first generic parameter. * - * @template TargetType - The expected type that will be compared with another type. + * @template TargetType - The target type that will be compared with expected type. */ export function notAssignableTo(): void; /** - * Test that the type provided in the first argument is not identical to expected type. + * Test that the expected type is not assignable to the type of the expression provided in the first argument. * - * @param targetValue - The expected value whose type will be compared with another type. + * @param expression - An expression whose type will be compared with expected type. */ -export function notAssignableTo(targetValue: TargetValueType): void; +export function notAssignableTo(expression: ExpressionType): void; export function notAssignableTo(): void { } diff --git a/source/lib/assertions/jest-like/api/not-identical-to.ts b/source/lib/assertions/jest-like/api/not-identical-to.ts index e031cd91..05ce0bf7 100644 --- a/source/lib/assertions/jest-like/api/not-identical-to.ts +++ b/source/lib/assertions/jest-like/api/not-identical-to.ts @@ -2,17 +2,17 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ /** - * Test that the type provided in the first generic parameter is not identical to expected type. + * Test that the expected type is not identical to the type provided in the first generic parameter. * - * @template TargetType - The expected type that will be compared with another type. + * @template TargetType - The target type that will be compared with expected type. */ export function notIdenticalTo(): void; /** - * Test that the type provided in the first argument is not identical to expected type. + * Test that the expected type is not identical to the type of the expression provided in the first argument. * - * @param targetValue - The expected value whose type will be compared with another type. + * @param expression - An expression whose type will be compared with expected type. */ -export function notIdenticalTo(targetValue: TargetValueType): void; +export function notIdenticalTo(expression: ExpressionType): void; export function notIdenticalTo(): void { } diff --git a/source/lib/assertions/jest-like/assert.ts b/source/lib/assertions/jest-like/assert.ts index a43cd602..dd0d718a 100644 --- a/source/lib/assertions/jest-like/assert.ts +++ b/source/lib/assertions/jest-like/assert.ts @@ -11,10 +11,10 @@ export function assertType(): AssertTypeAPI; /** * Create a type assertion holder from the type provided in the first argument. * - * @param expectedValue - The expected value whose type will be compared with another type. + * @param expression - The expected expression whose type will be compared with another type. */ -export function assertType(expectedValue: ExpectedValueType): AssertTypeAPI; +export function assertType(expression: ExpressionType): AssertTypeAPI; -export function assertType(): AssertTypeAPI { +export function assertType() { return api; } From f612a70f969fd827ce36c30744ac4a0689c1aa2e Mon Sep 17 00:00:00 2001 From: skarab42 Date: Sun, 23 Oct 2022 12:48:03 +0200 Subject: [PATCH 13/17] Add `subtypeOf` assertion --- source/lib/assertions/jest-like/api/index.ts | 4 ++ .../jest-like/api/not-subtype-of.ts | 18 +++++ .../assertions/jest-like/api/subtype-of.ts | 18 +++++ .../assertions/jest-like/handlers/index.ts | 2 + .../jest-like/handlers/is-not-subtype.ts | 37 +++++++++++ .../jest-like/handlers/is-subtype.ts | 37 +++++++++++ source/lib/assertions/jest-like/index.ts | 8 ++- .../jest-like-api/not-subtype-of/index.d.ts | 0 .../jest-like-api/not-subtype-of/index.js | 0 .../not-subtype-of/index.test-d.ts | 65 +++++++++++++++++++ .../jest-like-api/not-subtype-of/package.json | 3 + .../jest-like-api/subtype-of/index.d.ts | 0 .../jest-like-api/subtype-of/index.js | 0 .../jest-like-api/subtype-of/index.test-d.ts | 65 +++++++++++++++++++ .../jest-like-api/subtype-of/package.json | 3 + source/test/jest-like-api.ts | 40 ++++++++++++ 16 files changed, 299 insertions(+), 1 deletion(-) create mode 100644 source/lib/assertions/jest-like/api/not-subtype-of.ts create mode 100644 source/lib/assertions/jest-like/api/subtype-of.ts create mode 100644 source/lib/assertions/jest-like/handlers/is-not-subtype.ts create mode 100644 source/lib/assertions/jest-like/handlers/is-subtype.ts create mode 100644 source/test/fixtures/jest-like-api/not-subtype-of/index.d.ts create mode 100644 source/test/fixtures/jest-like-api/not-subtype-of/index.js create mode 100644 source/test/fixtures/jest-like-api/not-subtype-of/index.test-d.ts create mode 100644 source/test/fixtures/jest-like-api/not-subtype-of/package.json create mode 100644 source/test/fixtures/jest-like-api/subtype-of/index.d.ts create mode 100644 source/test/fixtures/jest-like-api/subtype-of/index.js create mode 100644 source/test/fixtures/jest-like-api/subtype-of/index.test-d.ts create mode 100644 source/test/fixtures/jest-like-api/subtype-of/package.json diff --git a/source/lib/assertions/jest-like/api/index.ts b/source/lib/assertions/jest-like/api/index.ts index 64b66c2d..dcd6c999 100644 --- a/source/lib/assertions/jest-like/api/index.ts +++ b/source/lib/assertions/jest-like/api/index.ts @@ -1,15 +1,19 @@ import {assignableTo} from './assignable-to'; import {identicalTo} from './identical-to'; +import {subtypeOf} from './subtype-of'; import {notAssignableTo} from './not-assignable-to'; import {notIdenticalTo} from './not-identical-to'; +import {notSubtypeOf} from './not-subtype-of'; export const api = { assignableTo, identicalTo, + subtypeOf, not: { assignableTo: notAssignableTo, identicalTo: notIdenticalTo, + subtypeOf: notSubtypeOf }, }; diff --git a/source/lib/assertions/jest-like/api/not-subtype-of.ts b/source/lib/assertions/jest-like/api/not-subtype-of.ts new file mode 100644 index 00000000..a2d7d785 --- /dev/null +++ b/source/lib/assertions/jest-like/api/not-subtype-of.ts @@ -0,0 +1,18 @@ +/* eslint-disable @typescript-eslint/no-empty-function */ +/* eslint-disable @typescript-eslint/no-unused-vars */ + +/** + * Test that the expected type is not a subtype of the type provided in the first generic parameter. + * + * @template TargetType - The target type that will be compared with expected type. + */ +export function notSubtypeOf(): void; + +/** + * Test that the expected type is not a subtype of the type of the expression provided in the first argument. + * + * @param expression - An expression whose type will be compared with expected type. + */ +export function notSubtypeOf(expression: ExpressionType): void; + +export function notSubtypeOf(): void { } diff --git a/source/lib/assertions/jest-like/api/subtype-of.ts b/source/lib/assertions/jest-like/api/subtype-of.ts new file mode 100644 index 00000000..9c272b06 --- /dev/null +++ b/source/lib/assertions/jest-like/api/subtype-of.ts @@ -0,0 +1,18 @@ +/* eslint-disable @typescript-eslint/no-empty-function */ +/* eslint-disable @typescript-eslint/no-unused-vars */ + +/** + * Test that the expected type is a subtype of the type provided in the first generic parameter. + * + * @template TargetType - The target type that will be compared with expected type. + */ +export function subtypeOf(): void; + +/** + * Test that the expected type is a subtype of the type of the expression provided in the first argument. + * + * @param expression - An expression whose type will be compared with expected type. + */ +export function subtypeOf(expression: ExpressionType): void; + +export function subtypeOf(): void { } diff --git a/source/lib/assertions/jest-like/handlers/index.ts b/source/lib/assertions/jest-like/handlers/index.ts index 643317bf..76ad33f7 100644 --- a/source/lib/assertions/jest-like/handlers/index.ts +++ b/source/lib/assertions/jest-like/handlers/index.ts @@ -1,5 +1,7 @@ export {isAssignable} from './is-assignable'; export {isIdentical} from './is-identical'; +export {isSubtype} from './is-subtype'; export {isNotAssignable} from './is-not-assignable'; export {isNotIdentical} from './is-not-identical'; +export {isNotSubtype} from './is-not-subtype'; diff --git a/source/lib/assertions/jest-like/handlers/is-not-subtype.ts b/source/lib/assertions/jest-like/handlers/is-not-subtype.ts new file mode 100644 index 00000000..e35488da --- /dev/null +++ b/source/lib/assertions/jest-like/handlers/is-not-subtype.ts @@ -0,0 +1,37 @@ +import {TypeChecker} from '@tsd/typescript'; +import {Diagnostic} from '../../../interfaces'; +import {makeDiagnostic} from '../../../utils'; +import {JestLikeAssertionNodes} from '..'; +import {getTypes} from '../util'; + +export const isNotSubtype = (checker: TypeChecker, nodes: JestLikeAssertionNodes): Diagnostic[] => { + const diagnostics: Diagnostic[] = []; + + if (!nodes) { + return diagnostics; + } + + for (const node of nodes) { + const [expectedNode, targetNode] = node; + + const expected = getTypes(expectedNode, checker); + + if (expected.diagnostic) { + diagnostics.push(expected.diagnostic); + continue; + } + + const target = getTypes(targetNode, checker); + + if (target.diagnostic) { + diagnostics.push(target.diagnostic); + continue; + } + + if (checker.isTypeSubtypeOf(expected.type, target.type)) { + diagnostics.push(makeDiagnostic(expectedNode, `Expected type \`${checker.typeToString(expected.type)}\` is a subtype of \`${checker.typeToString(target.type)}\`.`)); + } + } + + return diagnostics; +}; diff --git a/source/lib/assertions/jest-like/handlers/is-subtype.ts b/source/lib/assertions/jest-like/handlers/is-subtype.ts new file mode 100644 index 00000000..03adc3f9 --- /dev/null +++ b/source/lib/assertions/jest-like/handlers/is-subtype.ts @@ -0,0 +1,37 @@ +import {TypeChecker} from '@tsd/typescript'; +import {Diagnostic} from '../../../interfaces'; +import {makeDiagnostic} from '../../../utils'; +import {JestLikeAssertionNodes} from '..'; +import {getTypes} from '../util'; + +export const isSubtype = (checker: TypeChecker, nodes: JestLikeAssertionNodes): Diagnostic[] => { + const diagnostics: Diagnostic[] = []; + + if (!nodes) { + return diagnostics; + } + + for (const node of nodes) { + const [expectedNode, targetNode] = node; + + const expected = getTypes(expectedNode, checker); + + if (expected.diagnostic) { + diagnostics.push(expected.diagnostic); + continue; + } + + const target = getTypes(targetNode, checker); + + if (target.diagnostic) { + diagnostics.push(target.diagnostic); + continue; + } + + if (!checker.isTypeSubtypeOf(expected.type, target.type)) { + diagnostics.push(makeDiagnostic(expectedNode, `Expected type \`${checker.typeToString(expected.type)}\` is not a subtype of \`${checker.typeToString(target.type)}\`.`)); + } + } + + return diagnostics; +}; diff --git a/source/lib/assertions/jest-like/index.ts b/source/lib/assertions/jest-like/index.ts index 59415f6f..c1655fed 100644 --- a/source/lib/assertions/jest-like/index.ts +++ b/source/lib/assertions/jest-like/index.ts @@ -4,7 +4,9 @@ import { isAssignable, isIdentical, isNotAssignable, - isNotIdentical + isNotIdentical, + isNotSubtype, + isSubtype } from '../jest-like/handlers'; export type JestLikeAssertionNodes = Set<[CallExpression, CallExpression]>; @@ -25,18 +27,22 @@ export type JestLikeAssertions = Map; export enum JestLikeAssertion { ASSIGNABLE_TO = 'assignableTo', IDENTICAL_TO = 'identicalTo', + SUBTYPE_OF = 'subtypeOf', NOT_ASSIGNABLE_TO = 'notAssignableTo', NOT_IDENTICAL_TO = 'notIdenticalTo', + NOT_SUBTYPE_OF = 'notSubtypeOf', } // List of diagnostic handlers attached to the assertion const assertionHandlers: JestLikeAssertionHandlers = new Map([ [JestLikeAssertion.ASSIGNABLE_TO, isAssignable], [JestLikeAssertion.IDENTICAL_TO, isIdentical], + [JestLikeAssertion.SUBTYPE_OF, isSubtype], [JestLikeAssertion.NOT_ASSIGNABLE_TO, isNotAssignable], [JestLikeAssertion.NOT_IDENTICAL_TO, isNotIdentical], + [JestLikeAssertion.NOT_SUBTYPE_OF, isNotSubtype], ]); /** diff --git a/source/test/fixtures/jest-like-api/not-subtype-of/index.d.ts b/source/test/fixtures/jest-like-api/not-subtype-of/index.d.ts new file mode 100644 index 00000000..e69de29b diff --git a/source/test/fixtures/jest-like-api/not-subtype-of/index.js b/source/test/fixtures/jest-like-api/not-subtype-of/index.js new file mode 100644 index 00000000..e69de29b diff --git a/source/test/fixtures/jest-like-api/not-subtype-of/index.test-d.ts b/source/test/fixtures/jest-like-api/not-subtype-of/index.test-d.ts new file mode 100644 index 00000000..9bc3e291 --- /dev/null +++ b/source/test/fixtures/jest-like-api/not-subtype-of/index.test-d.ts @@ -0,0 +1,65 @@ +import {assertType} from '../../../..'; + +declare const fooString: string; +declare const fooNumber: number; + +// Shoul pass +assertType().not.subtypeOf(); +assertType().not.subtypeOf(fooString); +assertType(fooString).not.subtypeOf(); +assertType(fooString).not.subtypeOf(fooNumber); + +// Should fail +assertType().not.subtypeOf(); +assertType().not.subtypeOf(fooString); +assertType(fooString).not.subtypeOf(); +assertType(fooString).not.subtypeOf(fooString); + +// Should pass with reversed order (assignable type) +assertType().not.subtypeOf<'foo'>(); +assertType().not.subtypeOf('foo'); +assertType(fooString).not.subtypeOf<'foo'>(); +assertType(fooString).not.subtypeOf('foo'); + +// Should fail with assignable type +assertType<'foo'>().not.subtypeOf(); +assertType<'foo'>().not.subtypeOf(fooString); +assertType('foo').not.subtypeOf(); +assertType('foo').not.subtypeOf(fooString); + +// Should handle generic, see https://github.com/SamVerschueren/tsd/issues/142 +declare const inferrable: () => T; + +// Should pass +assertType().not.subtypeOf(inferrable()); +assertType(inferrable()).not.subtypeOf(fooNumber); + +// Should fail +assertType<'foo'>().not.subtypeOf(inferrable()); +assertType(inferrable()).not.subtypeOf('foo'); + +/** +The assignment compatibility and subtyping rules differ only in that + +- the Any type is assignable to, but not a subtype of, all types, +- the primitive type Number is assignable to, but not a subtype of, all enum types, and +- an object type without a particular property is assignable to an object type in which that property is optional. + +See https://github.com/microsoft/TypeScript/blob/v4.2.4/doc/spec-ARCHIVED.md#3114-assignment-compatibility +*/ +enum Bar { B, A, R } +type Foo = {id: number; name?: string | undefined}; + +class Baz { + readonly id: number = 42; +} + +// Should pass (FI: the following test fail with `not.assignableTo`) +assertType().not.subtypeOf(); +assertType().not.subtypeOf(); +assertType().not.subtypeOf(); + +// Should fail +assertType().not.subtypeOf(); +assertType().not.subtypeOf(); +assertType({id: 42, name: 'nyan'}).not.subtypeOf(); diff --git a/source/test/fixtures/jest-like-api/not-subtype-of/package.json b/source/test/fixtures/jest-like-api/not-subtype-of/package.json new file mode 100644 index 00000000..de6dc1db --- /dev/null +++ b/source/test/fixtures/jest-like-api/not-subtype-of/package.json @@ -0,0 +1,3 @@ +{ + "name": "foo" +} diff --git a/source/test/fixtures/jest-like-api/subtype-of/index.d.ts b/source/test/fixtures/jest-like-api/subtype-of/index.d.ts new file mode 100644 index 00000000..e69de29b diff --git a/source/test/fixtures/jest-like-api/subtype-of/index.js b/source/test/fixtures/jest-like-api/subtype-of/index.js new file mode 100644 index 00000000..e69de29b diff --git a/source/test/fixtures/jest-like-api/subtype-of/index.test-d.ts b/source/test/fixtures/jest-like-api/subtype-of/index.test-d.ts new file mode 100644 index 00000000..110a270a --- /dev/null +++ b/source/test/fixtures/jest-like-api/subtype-of/index.test-d.ts @@ -0,0 +1,65 @@ +import {assertType} from '../../../..'; + +declare const fooString: string; +declare const fooNumber: number; + +// Should pass +assertType().subtypeOf(); +assertType().subtypeOf(fooString); +assertType(fooString).subtypeOf(); +assertType(fooString).subtypeOf(fooString); + +// Shoul fail +assertType().subtypeOf(); +assertType().subtypeOf(fooString); +assertType(fooString).subtypeOf(); +assertType(fooString).subtypeOf(fooNumber); + +// Should pass with assignable type +assertType<'foo'>().subtypeOf(); +assertType<'foo'>().subtypeOf(fooString); +assertType('foo').subtypeOf(); +assertType('foo').subtypeOf(fooString); + +// Should fail with reversed order (assignable type) +assertType().subtypeOf<'foo'>(); +assertType().subtypeOf('foo'); +assertType(fooString).subtypeOf<'foo'>(); +assertType(fooString).subtypeOf('foo'); + +// Should handle generic, see https://github.com/SamVerschueren/tsd/issues/142 +declare const inferrable: () => T; + +// Should pass +assertType<'foo'>().subtypeOf(inferrable()); +assertType(inferrable()).subtypeOf('foo'); + +// Should fail +assertType().subtypeOf(inferrable()); +assertType(inferrable()).subtypeOf(fooNumber); + +/** +The assignment compatibility and subtyping rules differ only in that + +- the Any type is assignable to, but not a subtype of, all types, +- the primitive type Number is assignable to, but not a subtype of, all enum types, and +- an object type without a particular property is assignable to an object type in which that property is optional. + +See https://github.com/microsoft/TypeScript/blob/v4.2.4/doc/spec-ARCHIVED.md#3114-assignment-compatibility +*/ +enum Bar { B, A, R } +type Foo = {id: number; name?: string | undefined}; + +class Baz { + readonly id: number = 42; +} + +// Should pass +assertType().subtypeOf(); +assertType().subtypeOf(); +assertType({id: 42, name: 'nyan'}).subtypeOf(); + +// Should fail (FI: the following test pass with `assignableTo`) +assertType().subtypeOf(); +assertType().subtypeOf(); +assertType().subtypeOf(); diff --git a/source/test/fixtures/jest-like-api/subtype-of/package.json b/source/test/fixtures/jest-like-api/subtype-of/package.json new file mode 100644 index 00000000..de6dc1db --- /dev/null +++ b/source/test/fixtures/jest-like-api/subtype-of/package.json @@ -0,0 +1,3 @@ +{ + "name": "foo" +} diff --git a/source/test/jest-like-api.ts b/source/test/jest-like-api.ts index bba5a2ee..cef698c5 100644 --- a/source/test/jest-like-api.ts +++ b/source/test/jest-like-api.ts @@ -86,6 +86,46 @@ test('not-assignable-to', async t => { ]); }); +test('subtype-of', async t => { + const diagnostics = await tsd({cwd: path.join(__dirname, 'fixtures/jest-like-api/subtype-of')}); + + verify(t, diagnostics, [ + [13, 0, 'error', 'Expected type `string` is not a subtype of `number`.'], + [14, 0, 'error', 'Expected type `number` is not a subtype of `string`.'], + [15, 0, 'error', 'Expected type `string` is not a subtype of `number`.'], + [16, 0, 'error', 'Expected type `string` is not a subtype of `number`.'], + [25, 0, 'error', 'Expected type `string` is not a subtype of `"foo"`.'], + [26, 0, 'error', 'Expected type `string` is not a subtype of `"foo"`.'], + [27, 0, 'error', 'Expected type `string` is not a subtype of `"foo"`.'], + [28, 0, 'error', 'Expected type `string` is not a subtype of `"foo"`.'], + [38, 0, 'error', 'Expected type `string` is not a subtype of `"foo"`.'], + [39, 0, 'error', 'Expected type `"foo"` is not a subtype of `number`.'], + [63, 0, 'error', 'Expected type `any` is not a subtype of `string`.'], + [64, 0, 'error', 'Expected type `number` is not a subtype of `Bar`.'], + [65, 0, 'error', 'Expected type `Baz` is not a subtype of `Foo`.'], + ]); +}); + +test('not-subtype-of', async t => { + const diagnostics = await tsd({cwd: path.join(__dirname, 'fixtures/jest-like-api/not-subtype-of')}); + + verify(t, diagnostics, [ + [13, 0, 'error', 'Expected type `string` is a subtype of `string`.'], + [14, 0, 'error', 'Expected type `string` is a subtype of `string`.'], + [15, 0, 'error', 'Expected type `string` is a subtype of `string`.'], + [16, 0, 'error', 'Expected type `string` is a subtype of `string`.'], + [25, 0, 'error', 'Expected type `"foo"` is a subtype of `string`.'], + [26, 0, 'error', 'Expected type `"foo"` is a subtype of `string`.'], + [27, 0, 'error', 'Expected type `"foo"` is a subtype of `string`.'], + [28, 0, 'error', 'Expected type `"foo"` is a subtype of `string`.'], + [38, 0, 'error', 'Expected type `"foo"` is a subtype of `"foo"`.'], + [39, 0, 'error', 'Expected type `"foo"` is a subtype of `"foo"`.'], + [63, 0, 'error', 'Expected type `string` is a subtype of `any`.'], + [64, 0, 'error', 'Expected type `Bar` is a subtype of `number`.'], + [65, 0, 'error', 'Expected type `{ id: number; name: string; }` is a subtype of `Foo`.'], + ]); +}); + // // Debug // test('debug', async () => { // const diagnostics = await tsd({cwd: path.join(__dirname, 'fixtures/jest-like-api/identicality')}); From da3196595ef26944fa8934180a3229cea639a08b Mon Sep 17 00:00:00 2001 From: skarab42 Date: Sun, 23 Oct 2022 12:53:28 +0200 Subject: [PATCH 14/17] Test that `assignableTo` and `subtypeOf` behave differently --- .../assignable-to/index.test-d.ts | 26 +++++++++++++++++++ .../not-assignable-to/index.test-d.ts | 26 +++++++++++++++++++ source/test/jest-like-api.ts | 6 +++++ 3 files changed, 58 insertions(+) diff --git a/source/test/fixtures/jest-like-api/assignable-to/index.test-d.ts b/source/test/fixtures/jest-like-api/assignable-to/index.test-d.ts index 7d3534ed..e3c12fe6 100644 --- a/source/test/fixtures/jest-like-api/assignable-to/index.test-d.ts +++ b/source/test/fixtures/jest-like-api/assignable-to/index.test-d.ts @@ -37,3 +37,29 @@ assertType(inferrable()).assignableTo('foo'); // Should fail assertType().assignableTo(inferrable()); assertType(inferrable()).assignableTo(fooNumber); + +/** +The assignment compatibility and subtyping rules differ only in that + +- the Any type is assignable to, but not a subtype of, all types, +- the primitive type Number is assignable to, but not a subtype of, all enum types, and +- an object type without a particular property is assignable to an object type in which that property is optional. + +See https://github.com/microsoft/TypeScript/blob/v4.2.4/doc/spec-ARCHIVED.md#3114-assignment-compatibility +*/ +enum Bar { B, A, R } +type Foo = {id: number; name?: string | undefined}; + +class Baz { + readonly id: number = 42; +} + +// Should pass +assertType().assignableTo(); +assertType().assignableTo(); +assertType({id: 42, name: 'nyan'}).assignableTo(); + +// Should also pass (FI: the following test fail with `subtypeOf`) +assertType().assignableTo(); +assertType().assignableTo(); +assertType().assignableTo(); diff --git a/source/test/fixtures/jest-like-api/not-assignable-to/index.test-d.ts b/source/test/fixtures/jest-like-api/not-assignable-to/index.test-d.ts index c4c71b54..94d6f815 100644 --- a/source/test/fixtures/jest-like-api/not-assignable-to/index.test-d.ts +++ b/source/test/fixtures/jest-like-api/not-assignable-to/index.test-d.ts @@ -37,3 +37,29 @@ assertType(inferrable()).not.assignableTo(fooNumber); // Should fail assertType<'foo'>().not.assignableTo(inferrable()); assertType(inferrable()).not.assignableTo('foo'); + +/** +The assignment compatibility and subtyping rules differ only in that + +- the Any type is assignable to, but not a subtype of, all types, +- the primitive type Number is assignable to, but not a subtype of, all enum types, and +- an object type without a particular property is assignable to an object type in which that property is optional. + +See https://github.com/microsoft/TypeScript/blob/v4.2.4/doc/spec-ARCHIVED.md#3114-assignment-compatibility +*/ +enum Bar { B, A, R } +type Foo = {id: number; name?: string | undefined}; + +class Baz { + readonly id: number = 42; +} + +// Should fail +assertType().not.assignableTo(); +assertType().not.assignableTo(); +assertType({id: 42, name: 'nyan'}).not.assignableTo(); + +// Should also fail (FI: the following test pass with `subtypeOf`) +assertType().not.assignableTo(); +assertType().not.assignableTo(); +assertType().not.assignableTo(); diff --git a/source/test/jest-like-api.ts b/source/test/jest-like-api.ts index cef698c5..d0c2353b 100644 --- a/source/test/jest-like-api.ts +++ b/source/test/jest-like-api.ts @@ -83,6 +83,12 @@ test('not-assignable-to', async t => { [28, 0, 'error', 'Expected type `"foo"` is assignable to type `string`.'], [38, 0, 'error', 'Expected type `"foo"` is assignable to type `"foo"`.'], [39, 0, 'error', 'Expected type `"foo"` is assignable to type `"foo"`.'], + [58, 0, 'error', 'Expected type `string` is assignable to type `any`.'], + [59, 0, 'error', 'Expected type `Bar` is assignable to type `number`.'], + [60, 0, 'error', 'Expected type `{ id: number; name: string; }` is assignable to type `Foo`.'], + [63, 0, 'error', 'Expected type `any` is assignable to type `string`.'], + [64, 0, 'error', 'Expected type `number` is assignable to type `Bar`.'], + [65, 0, 'error', 'Expected type `Baz` is assignable to type `Foo`.'], ]); }); From 795236682c001e8d8b1440cf24afe5822c4c6458 Mon Sep 17 00:00:00 2001 From: skarab42 Date: Sun, 23 Oct 2022 13:41:45 +0200 Subject: [PATCH 15/17] Rename handlers to match api names --- .../{is-assignable.ts => assignable-to.ts} | 2 +- .../{is-identical.ts => identical-to.ts} | 2 +- .../assertions/jest-like/handlers/index.ts | 12 +++++----- ...not-assignable.ts => not-assignable-to.ts} | 2 +- ...s-not-identical.ts => not-identical-to.ts} | 2 +- .../{is-not-subtype.ts => not-subtype-of.ts} | 2 +- .../handlers/{is-subtype.ts => subtype-of.ts} | 2 +- source/lib/assertions/jest-like/index.ts | 24 +++++++++---------- 8 files changed, 24 insertions(+), 24 deletions(-) rename source/lib/assertions/jest-like/handlers/{is-assignable.ts => assignable-to.ts} (93%) rename source/lib/assertions/jest-like/handlers/{is-identical.ts => identical-to.ts} (95%) rename source/lib/assertions/jest-like/handlers/{is-not-assignable.ts => not-assignable-to.ts} (93%) rename source/lib/assertions/jest-like/handlers/{is-not-identical.ts => not-identical-to.ts} (93%) rename source/lib/assertions/jest-like/handlers/{is-not-subtype.ts => not-subtype-of.ts} (93%) rename source/lib/assertions/jest-like/handlers/{is-subtype.ts => subtype-of.ts} (93%) diff --git a/source/lib/assertions/jest-like/handlers/is-assignable.ts b/source/lib/assertions/jest-like/handlers/assignable-to.ts similarity index 93% rename from source/lib/assertions/jest-like/handlers/is-assignable.ts rename to source/lib/assertions/jest-like/handlers/assignable-to.ts index 836d9136..f9479be7 100644 --- a/source/lib/assertions/jest-like/handlers/is-assignable.ts +++ b/source/lib/assertions/jest-like/handlers/assignable-to.ts @@ -4,7 +4,7 @@ import {makeDiagnostic} from '../../../utils'; import {JestLikeAssertionNodes} from '..'; import {getTypes} from '../util'; -export const isAssignable = (checker: TypeChecker, nodes: JestLikeAssertionNodes): Diagnostic[] => { +export const assignableTo = (checker: TypeChecker, nodes: JestLikeAssertionNodes): Diagnostic[] => { const diagnostics: Diagnostic[] = []; if (!nodes) { diff --git a/source/lib/assertions/jest-like/handlers/is-identical.ts b/source/lib/assertions/jest-like/handlers/identical-to.ts similarity index 95% rename from source/lib/assertions/jest-like/handlers/is-identical.ts rename to source/lib/assertions/jest-like/handlers/identical-to.ts index f9fe33f6..a564fe9e 100644 --- a/source/lib/assertions/jest-like/handlers/is-identical.ts +++ b/source/lib/assertions/jest-like/handlers/identical-to.ts @@ -4,7 +4,7 @@ import {makeDiagnostic} from '../../../utils'; import {JestLikeAssertionNodes} from '..'; import {getTypes} from '../util'; -export const isIdentical = (checker: TypeChecker, nodes: JestLikeAssertionNodes): Diagnostic[] => { +export const identicalTo = (checker: TypeChecker, nodes: JestLikeAssertionNodes): Diagnostic[] => { const diagnostics: Diagnostic[] = []; if (!nodes) { diff --git a/source/lib/assertions/jest-like/handlers/index.ts b/source/lib/assertions/jest-like/handlers/index.ts index 76ad33f7..3636ad03 100644 --- a/source/lib/assertions/jest-like/handlers/index.ts +++ b/source/lib/assertions/jest-like/handlers/index.ts @@ -1,7 +1,7 @@ -export {isAssignable} from './is-assignable'; -export {isIdentical} from './is-identical'; -export {isSubtype} from './is-subtype'; +export {assignableTo} from './assignable-to'; +export {identicalTo} from './identical-to'; +export {subtypeOf} from './subtype-of'; -export {isNotAssignable} from './is-not-assignable'; -export {isNotIdentical} from './is-not-identical'; -export {isNotSubtype} from './is-not-subtype'; +export {notAssignableTo} from './not-assignable-to'; +export {notIdenticalTo} from './not-identical-to'; +export {notSubtypeOf} from './not-subtype-of'; diff --git a/source/lib/assertions/jest-like/handlers/is-not-assignable.ts b/source/lib/assertions/jest-like/handlers/not-assignable-to.ts similarity index 93% rename from source/lib/assertions/jest-like/handlers/is-not-assignable.ts rename to source/lib/assertions/jest-like/handlers/not-assignable-to.ts index d5d6d976..9a6f9804 100644 --- a/source/lib/assertions/jest-like/handlers/is-not-assignable.ts +++ b/source/lib/assertions/jest-like/handlers/not-assignable-to.ts @@ -4,7 +4,7 @@ import {makeDiagnostic} from '../../../utils'; import {JestLikeAssertionNodes} from '..'; import {getTypes} from '../util'; -export const isNotAssignable = (checker: TypeChecker, nodes: JestLikeAssertionNodes): Diagnostic[] => { +export const notAssignableTo = (checker: TypeChecker, nodes: JestLikeAssertionNodes): Diagnostic[] => { const diagnostics: Diagnostic[] = []; if (!nodes) { diff --git a/source/lib/assertions/jest-like/handlers/is-not-identical.ts b/source/lib/assertions/jest-like/handlers/not-identical-to.ts similarity index 93% rename from source/lib/assertions/jest-like/handlers/is-not-identical.ts rename to source/lib/assertions/jest-like/handlers/not-identical-to.ts index fa5d01a6..14f9334d 100644 --- a/source/lib/assertions/jest-like/handlers/is-not-identical.ts +++ b/source/lib/assertions/jest-like/handlers/not-identical-to.ts @@ -4,7 +4,7 @@ import {makeDiagnostic} from '../../../utils'; import {JestLikeAssertionNodes} from '..'; import {getTypes} from '../util'; -export const isNotIdentical = (checker: TypeChecker, nodes: JestLikeAssertionNodes): Diagnostic[] => { +export const notIdenticalTo = (checker: TypeChecker, nodes: JestLikeAssertionNodes): Diagnostic[] => { const diagnostics: Diagnostic[] = []; if (!nodes) { diff --git a/source/lib/assertions/jest-like/handlers/is-not-subtype.ts b/source/lib/assertions/jest-like/handlers/not-subtype-of.ts similarity index 93% rename from source/lib/assertions/jest-like/handlers/is-not-subtype.ts rename to source/lib/assertions/jest-like/handlers/not-subtype-of.ts index e35488da..e34f7165 100644 --- a/source/lib/assertions/jest-like/handlers/is-not-subtype.ts +++ b/source/lib/assertions/jest-like/handlers/not-subtype-of.ts @@ -4,7 +4,7 @@ import {makeDiagnostic} from '../../../utils'; import {JestLikeAssertionNodes} from '..'; import {getTypes} from '../util'; -export const isNotSubtype = (checker: TypeChecker, nodes: JestLikeAssertionNodes): Diagnostic[] => { +export const notSubtypeOf = (checker: TypeChecker, nodes: JestLikeAssertionNodes): Diagnostic[] => { const diagnostics: Diagnostic[] = []; if (!nodes) { diff --git a/source/lib/assertions/jest-like/handlers/is-subtype.ts b/source/lib/assertions/jest-like/handlers/subtype-of.ts similarity index 93% rename from source/lib/assertions/jest-like/handlers/is-subtype.ts rename to source/lib/assertions/jest-like/handlers/subtype-of.ts index 03adc3f9..fdb83f07 100644 --- a/source/lib/assertions/jest-like/handlers/is-subtype.ts +++ b/source/lib/assertions/jest-like/handlers/subtype-of.ts @@ -4,7 +4,7 @@ import {makeDiagnostic} from '../../../utils'; import {JestLikeAssertionNodes} from '..'; import {getTypes} from '../util'; -export const isSubtype = (checker: TypeChecker, nodes: JestLikeAssertionNodes): Diagnostic[] => { +export const subtypeOf = (checker: TypeChecker, nodes: JestLikeAssertionNodes): Diagnostic[] => { const diagnostics: Diagnostic[] = []; if (!nodes) { diff --git a/source/lib/assertions/jest-like/index.ts b/source/lib/assertions/jest-like/index.ts index c1655fed..f2d8f45c 100644 --- a/source/lib/assertions/jest-like/index.ts +++ b/source/lib/assertions/jest-like/index.ts @@ -1,12 +1,12 @@ import {CallExpression, TypeChecker} from '@tsd/typescript'; import {Diagnostic} from '../../interfaces'; import { - isAssignable, - isIdentical, - isNotAssignable, - isNotIdentical, - isNotSubtype, - isSubtype + assignableTo, + identicalTo, + notAssignableTo, + notIdenticalTo, + notSubtypeOf, + subtypeOf } from '../jest-like/handlers'; export type JestLikeAssertionNodes = Set<[CallExpression, CallExpression]>; @@ -36,13 +36,13 @@ export enum JestLikeAssertion { // List of diagnostic handlers attached to the assertion const assertionHandlers: JestLikeAssertionHandlers = new Map([ - [JestLikeAssertion.ASSIGNABLE_TO, isAssignable], - [JestLikeAssertion.IDENTICAL_TO, isIdentical], - [JestLikeAssertion.SUBTYPE_OF, isSubtype], + [JestLikeAssertion.ASSIGNABLE_TO, assignableTo], + [JestLikeAssertion.IDENTICAL_TO, identicalTo], + [JestLikeAssertion.SUBTYPE_OF, subtypeOf], - [JestLikeAssertion.NOT_ASSIGNABLE_TO, isNotAssignable], - [JestLikeAssertion.NOT_IDENTICAL_TO, isNotIdentical], - [JestLikeAssertion.NOT_SUBTYPE_OF, isNotSubtype], + [JestLikeAssertion.NOT_ASSIGNABLE_TO, notAssignableTo], + [JestLikeAssertion.NOT_IDENTICAL_TO, notIdenticalTo], + [JestLikeAssertion.NOT_SUBTYPE_OF, notSubtypeOf], ]); /** From 2b8fad58c9e58e5ea988f6e237526aa5d7bbffe5 Mon Sep 17 00:00:00 2001 From: skarab42 Date: Sun, 23 Oct 2022 13:53:32 +0200 Subject: [PATCH 16/17] Removes the need to explicitly declare handlers --- source/lib/assertions/jest-like/index.ts | 44 +++--------------------- 1 file changed, 4 insertions(+), 40 deletions(-) diff --git a/source/lib/assertions/jest-like/index.ts b/source/lib/assertions/jest-like/index.ts index f2d8f45c..f48ba9eb 100644 --- a/source/lib/assertions/jest-like/index.ts +++ b/source/lib/assertions/jest-like/index.ts @@ -1,50 +1,14 @@ import {CallExpression, TypeChecker} from '@tsd/typescript'; import {Diagnostic} from '../../interfaces'; -import { - assignableTo, - identicalTo, - notAssignableTo, - notIdenticalTo, - notSubtypeOf, - subtypeOf -} from '../jest-like/handlers'; -export type JestLikeAssertionNodes = Set<[CallExpression, CallExpression]>; +import * as handlers from '../jest-like/handlers'; -/** - * A handler is a method which accepts the TypeScript type checker together with a set of assertion nodes. The type checker - * can be used to retrieve extra type information from these nodes in order to determine a list of diagnostics. - * - * @param typeChecker - The TypeScript type checker. - * @param nodes - List of nodes. - * @returns List of diagnostics. - */ +export type JestLikeAssertion = keyof typeof handlers; +export type JestLikeAssertionNodes = Set<[CallExpression, CallExpression]>; export type JestLikeHandler = (typeChecker: TypeChecker, nodes: JestLikeAssertionNodes) => Diagnostic[]; - export type JestLikeAssertionHandlers = Map; export type JestLikeAssertions = Map; -export enum JestLikeAssertion { - ASSIGNABLE_TO = 'assignableTo', - IDENTICAL_TO = 'identicalTo', - SUBTYPE_OF = 'subtypeOf', - - NOT_ASSIGNABLE_TO = 'notAssignableTo', - NOT_IDENTICAL_TO = 'notIdenticalTo', - NOT_SUBTYPE_OF = 'notSubtypeOf', -} - -// List of diagnostic handlers attached to the assertion -const assertionHandlers: JestLikeAssertionHandlers = new Map([ - [JestLikeAssertion.ASSIGNABLE_TO, assignableTo], - [JestLikeAssertion.IDENTICAL_TO, identicalTo], - [JestLikeAssertion.SUBTYPE_OF, subtypeOf], - - [JestLikeAssertion.NOT_ASSIGNABLE_TO, notAssignableTo], - [JestLikeAssertion.NOT_IDENTICAL_TO, notIdenticalTo], - [JestLikeAssertion.NOT_SUBTYPE_OF, notSubtypeOf], -]); - /** * Returns a list of diagnostics based on the assertions provided. * @@ -56,7 +20,7 @@ export const jestLikeHandle = (typeChecker: TypeChecker, assertions: JestLikeAss const diagnostics: Diagnostic[] = []; for (const [assertion, nodes] of assertions) { - const handler = assertionHandlers.get(assertion); + const handler = handlers[assertion]; if (!handler) { // Ignore these assertions as no handler is found From 416e58ab43a7d2326eda488d651eaf793fac3c44 Mon Sep 17 00:00:00 2001 From: skarab42 Date: Sun, 23 Oct 2022 18:13:12 +0200 Subject: [PATCH 17/17] Add `toThrowError` assertion --- source/lib/assertions/jest-like/api/index.ts | 3 + .../jest-like/api/to-throw-error.ts | 29 +++++++++ .../jest-like/handlers/assignable-to.ts | 13 ++-- .../jest-like/handlers/identical-to.ts | 21 ++++--- .../assertions/jest-like/handlers/index.ts | 2 + .../jest-like/handlers/not-assignable-to.ts | 13 ++-- .../jest-like/handlers/not-identical-to.ts | 13 ++-- .../jest-like/handlers/not-subtype-of.ts | 13 ++-- .../jest-like/handlers/subtype-of.ts | 13 ++-- .../jest-like/handlers/to-throw-error.ts | 50 ++++++++++++++++ source/lib/assertions/jest-like/index.ts | 20 +++++-- source/lib/assertions/jest-like/util.ts | 26 ++++++-- source/lib/compiler.ts | 59 ++++++++++++++++--- .../jest-like-api/to-throw-error/index.d.ts | 0 .../jest-like-api/to-throw-error/index.js | 0 .../to-throw-error/index.test-d.ts | 15 +++++ .../jest-like-api/to-throw-error/package.json | 3 + source/test/jest-like-api.ts | 11 ++++ source/test/test.ts | 1 + 19 files changed, 239 insertions(+), 66 deletions(-) create mode 100644 source/lib/assertions/jest-like/api/to-throw-error.ts create mode 100644 source/lib/assertions/jest-like/handlers/to-throw-error.ts create mode 100644 source/test/fixtures/jest-like-api/to-throw-error/index.d.ts create mode 100644 source/test/fixtures/jest-like-api/to-throw-error/index.js create mode 100644 source/test/fixtures/jest-like-api/to-throw-error/index.test-d.ts create mode 100644 source/test/fixtures/jest-like-api/to-throw-error/package.json diff --git a/source/lib/assertions/jest-like/api/index.ts b/source/lib/assertions/jest-like/api/index.ts index dcd6c999..aebd1dfa 100644 --- a/source/lib/assertions/jest-like/api/index.ts +++ b/source/lib/assertions/jest-like/api/index.ts @@ -6,10 +6,13 @@ import {notAssignableTo} from './not-assignable-to'; import {notIdenticalTo} from './not-identical-to'; import {notSubtypeOf} from './not-subtype-of'; +import {toThrowError} from './to-throw-error'; + export const api = { assignableTo, identicalTo, subtypeOf, + toThrowError, not: { assignableTo: notAssignableTo, identicalTo: notIdenticalTo, diff --git a/source/lib/assertions/jest-like/api/to-throw-error.ts b/source/lib/assertions/jest-like/api/to-throw-error.ts new file mode 100644 index 00000000..57e33332 --- /dev/null +++ b/source/lib/assertions/jest-like/api/to-throw-error.ts @@ -0,0 +1,29 @@ +/* eslint-disable @typescript-eslint/no-empty-function */ + +/** + * Test that the expected type throw a type error. + */ +export function toThrowError(): void; + +/** + * Test that the expected type throw a type error with expected code. + * + * @param code - The expected error code. + */ +export function toThrowError(code: Code): void; + +/** + * Test that the expected type throw a type error with expected code. + * + * @param regexp - A regular expression that must match the message. + */ +export function toThrowError(regexp: Pattern): void; + +/** + * Test that the expected type throw a type error with expected message. + * + * @param message - The expected error message or a regular expression to match the error message. + */ +export function toThrowError(message: Message): void; + +export function toThrowError(): void { } diff --git a/source/lib/assertions/jest-like/handlers/assignable-to.ts b/source/lib/assertions/jest-like/handlers/assignable-to.ts index f9479be7..164e0521 100644 --- a/source/lib/assertions/jest-like/handlers/assignable-to.ts +++ b/source/lib/assertions/jest-like/handlers/assignable-to.ts @@ -1,10 +1,9 @@ -import {TypeChecker} from '@tsd/typescript'; import {Diagnostic} from '../../../interfaces'; import {makeDiagnostic} from '../../../utils'; -import {JestLikeAssertionNodes} from '..'; +import {JestLikeAssertionNodes, JestLikeContext} from '..'; import {getTypes} from '../util'; -export const assignableTo = (checker: TypeChecker, nodes: JestLikeAssertionNodes): Diagnostic[] => { +export const assignableTo = ({typeChecker}: JestLikeContext, nodes: JestLikeAssertionNodes): Diagnostic[] => { const diagnostics: Diagnostic[] = []; if (!nodes) { @@ -14,22 +13,22 @@ export const assignableTo = (checker: TypeChecker, nodes: JestLikeAssertionNodes for (const node of nodes) { const [expectedNode, targetNode] = node; - const expected = getTypes(expectedNode, checker); + const expected = getTypes(expectedNode, typeChecker); if (expected.diagnostic) { diagnostics.push(expected.diagnostic); continue; } - const target = getTypes(targetNode, checker); + const target = getTypes(targetNode, typeChecker); if (target.diagnostic) { diagnostics.push(target.diagnostic); continue; } - if (!checker.isTypeAssignableTo(expected.type, target.type)) { - diagnostics.push(makeDiagnostic(expectedNode, `Expected type \`${checker.typeToString(expected.type)}\` is not assignable to type \`${checker.typeToString(target.type)}\`.`)); + if (!typeChecker.isTypeAssignableTo(expected.type, target.type)) { + diagnostics.push(makeDiagnostic(expectedNode, `Expected type \`${typeChecker.typeToString(expected.type)}\` is not assignable to type \`${typeChecker.typeToString(target.type)}\`.`)); } } diff --git a/source/lib/assertions/jest-like/handlers/identical-to.ts b/source/lib/assertions/jest-like/handlers/identical-to.ts index a564fe9e..acadc3c8 100644 --- a/source/lib/assertions/jest-like/handlers/identical-to.ts +++ b/source/lib/assertions/jest-like/handlers/identical-to.ts @@ -1,10 +1,9 @@ -import {TypeChecker} from '@tsd/typescript'; import {Diagnostic} from '../../../interfaces'; import {makeDiagnostic} from '../../../utils'; -import {JestLikeAssertionNodes} from '..'; +import {JestLikeAssertionNodes, JestLikeContext} from '..'; import {getTypes} from '../util'; -export const identicalTo = (checker: TypeChecker, nodes: JestLikeAssertionNodes): Diagnostic[] => { +export const identicalTo = ({typeChecker}: JestLikeContext, nodes: JestLikeAssertionNodes): Diagnostic[] => { const diagnostics: Diagnostic[] = []; if (!nodes) { @@ -14,32 +13,32 @@ export const identicalTo = (checker: TypeChecker, nodes: JestLikeAssertionNodes) for (const node of nodes) { const [expectedNode, targetNode] = node; - const expected = getTypes(expectedNode, checker); + const expected = getTypes(expectedNode, typeChecker); if (expected.diagnostic) { diagnostics.push(expected.diagnostic); continue; } - const target = getTypes(targetNode, checker); + const target = getTypes(targetNode, typeChecker); if (target.diagnostic) { diagnostics.push(target.diagnostic); continue; } - if (!checker.isTypeAssignableTo(expected.type, target.type)) { - diagnostics.push(makeDiagnostic(expectedNode, `Expected type \`${checker.typeToString(expected.type)}\` is declared too wide for type \`${checker.typeToString(target.type)}\`.`)); + if (!typeChecker.isTypeAssignableTo(expected.type, target.type)) { + diagnostics.push(makeDiagnostic(expectedNode, `Expected type \`${typeChecker.typeToString(expected.type)}\` is declared too wide for type \`${typeChecker.typeToString(target.type)}\`.`)); continue; } - if (!checker.isTypeAssignableTo(target.type, expected.type)) { - diagnostics.push(makeDiagnostic(expectedNode, `Expected type \`${checker.typeToString(expected.type)}\` is declared too short for type \`${checker.typeToString(target.type)}\`.`)); + if (!typeChecker.isTypeAssignableTo(target.type, expected.type)) { + diagnostics.push(makeDiagnostic(expectedNode, `Expected type \`${typeChecker.typeToString(expected.type)}\` is declared too short for type \`${typeChecker.typeToString(target.type)}\`.`)); continue; } - if (!checker.isTypeIdenticalTo(expected.type, target.type)) { - diagnostics.push(makeDiagnostic(expectedNode, `Expected type \`${checker.typeToString(expected.type)}\` is not identical to type \`${checker.typeToString(target.type)}\`.`)); + if (!typeChecker.isTypeIdenticalTo(expected.type, target.type)) { + diagnostics.push(makeDiagnostic(expectedNode, `Expected type \`${typeChecker.typeToString(expected.type)}\` is not identical to type \`${typeChecker.typeToString(target.type)}\`.`)); } } diff --git a/source/lib/assertions/jest-like/handlers/index.ts b/source/lib/assertions/jest-like/handlers/index.ts index 3636ad03..2742f11f 100644 --- a/source/lib/assertions/jest-like/handlers/index.ts +++ b/source/lib/assertions/jest-like/handlers/index.ts @@ -5,3 +5,5 @@ export {subtypeOf} from './subtype-of'; export {notAssignableTo} from './not-assignable-to'; export {notIdenticalTo} from './not-identical-to'; export {notSubtypeOf} from './not-subtype-of'; + +export {toThrowError} from './to-throw-error'; diff --git a/source/lib/assertions/jest-like/handlers/not-assignable-to.ts b/source/lib/assertions/jest-like/handlers/not-assignable-to.ts index 9a6f9804..68ffb2f4 100644 --- a/source/lib/assertions/jest-like/handlers/not-assignable-to.ts +++ b/source/lib/assertions/jest-like/handlers/not-assignable-to.ts @@ -1,10 +1,9 @@ -import {TypeChecker} from '@tsd/typescript'; import {Diagnostic} from '../../../interfaces'; import {makeDiagnostic} from '../../../utils'; -import {JestLikeAssertionNodes} from '..'; +import {JestLikeAssertionNodes, JestLikeContext} from '..'; import {getTypes} from '../util'; -export const notAssignableTo = (checker: TypeChecker, nodes: JestLikeAssertionNodes): Diagnostic[] => { +export const notAssignableTo = ({typeChecker}: JestLikeContext, nodes: JestLikeAssertionNodes): Diagnostic[] => { const diagnostics: Diagnostic[] = []; if (!nodes) { @@ -14,22 +13,22 @@ export const notAssignableTo = (checker: TypeChecker, nodes: JestLikeAssertionNo for (const node of nodes) { const [expectedNode, targetNode] = node; - const expected = getTypes(expectedNode, checker); + const expected = getTypes(expectedNode, typeChecker); if (expected.diagnostic) { diagnostics.push(expected.diagnostic); continue; } - const target = getTypes(targetNode, checker); + const target = getTypes(targetNode, typeChecker); if (target.diagnostic) { diagnostics.push(target.diagnostic); continue; } - if (checker.isTypeAssignableTo(expected.type, target.type)) { - diagnostics.push(makeDiagnostic(expectedNode, `Expected type \`${checker.typeToString(expected.type)}\` is assignable to type \`${checker.typeToString(target.type)}\`.`)); + if (typeChecker.isTypeAssignableTo(expected.type, target.type)) { + diagnostics.push(makeDiagnostic(expectedNode, `Expected type \`${typeChecker.typeToString(expected.type)}\` is assignable to type \`${typeChecker.typeToString(target.type)}\`.`)); } } diff --git a/source/lib/assertions/jest-like/handlers/not-identical-to.ts b/source/lib/assertions/jest-like/handlers/not-identical-to.ts index 14f9334d..6c7a34b5 100644 --- a/source/lib/assertions/jest-like/handlers/not-identical-to.ts +++ b/source/lib/assertions/jest-like/handlers/not-identical-to.ts @@ -1,10 +1,9 @@ -import {TypeChecker} from '@tsd/typescript'; import {Diagnostic} from '../../../interfaces'; import {makeDiagnostic} from '../../../utils'; -import {JestLikeAssertionNodes} from '..'; +import {JestLikeAssertionNodes, JestLikeContext} from '..'; import {getTypes} from '../util'; -export const notIdenticalTo = (checker: TypeChecker, nodes: JestLikeAssertionNodes): Diagnostic[] => { +export const notIdenticalTo = ({typeChecker}: JestLikeContext, nodes: JestLikeAssertionNodes): Diagnostic[] => { const diagnostics: Diagnostic[] = []; if (!nodes) { @@ -14,22 +13,22 @@ export const notIdenticalTo = (checker: TypeChecker, nodes: JestLikeAssertionNod for (const node of nodes) { const [expectedNode, targetNode] = node; - const expected = getTypes(expectedNode, checker); + const expected = getTypes(expectedNode, typeChecker); if (expected.diagnostic) { diagnostics.push(expected.diagnostic); continue; } - const target = getTypes(targetNode, checker); + const target = getTypes(targetNode, typeChecker); if (target.diagnostic) { diagnostics.push(target.diagnostic); continue; } - if (checker.isTypeIdenticalTo(expected.type, target.type)) { - diagnostics.push(makeDiagnostic(expectedNode, `Expected type \`${checker.typeToString(expected.type)}\` is identical to type \`${checker.typeToString(target.type)}\`.`)); + if (typeChecker.isTypeIdenticalTo(expected.type, target.type)) { + diagnostics.push(makeDiagnostic(expectedNode, `Expected type \`${typeChecker.typeToString(expected.type)}\` is identical to type \`${typeChecker.typeToString(target.type)}\`.`)); } } diff --git a/source/lib/assertions/jest-like/handlers/not-subtype-of.ts b/source/lib/assertions/jest-like/handlers/not-subtype-of.ts index e34f7165..43e91dde 100644 --- a/source/lib/assertions/jest-like/handlers/not-subtype-of.ts +++ b/source/lib/assertions/jest-like/handlers/not-subtype-of.ts @@ -1,10 +1,9 @@ -import {TypeChecker} from '@tsd/typescript'; import {Diagnostic} from '../../../interfaces'; import {makeDiagnostic} from '../../../utils'; -import {JestLikeAssertionNodes} from '..'; +import {JestLikeAssertionNodes, JestLikeContext} from '..'; import {getTypes} from '../util'; -export const notSubtypeOf = (checker: TypeChecker, nodes: JestLikeAssertionNodes): Diagnostic[] => { +export const notSubtypeOf = ({typeChecker}: JestLikeContext, nodes: JestLikeAssertionNodes): Diagnostic[] => { const diagnostics: Diagnostic[] = []; if (!nodes) { @@ -14,22 +13,22 @@ export const notSubtypeOf = (checker: TypeChecker, nodes: JestLikeAssertionNodes for (const node of nodes) { const [expectedNode, targetNode] = node; - const expected = getTypes(expectedNode, checker); + const expected = getTypes(expectedNode, typeChecker); if (expected.diagnostic) { diagnostics.push(expected.diagnostic); continue; } - const target = getTypes(targetNode, checker); + const target = getTypes(targetNode, typeChecker); if (target.diagnostic) { diagnostics.push(target.diagnostic); continue; } - if (checker.isTypeSubtypeOf(expected.type, target.type)) { - diagnostics.push(makeDiagnostic(expectedNode, `Expected type \`${checker.typeToString(expected.type)}\` is a subtype of \`${checker.typeToString(target.type)}\`.`)); + if (typeChecker.isTypeSubtypeOf(expected.type, target.type)) { + diagnostics.push(makeDiagnostic(expectedNode, `Expected type \`${typeChecker.typeToString(expected.type)}\` is a subtype of \`${typeChecker.typeToString(target.type)}\`.`)); } } diff --git a/source/lib/assertions/jest-like/handlers/subtype-of.ts b/source/lib/assertions/jest-like/handlers/subtype-of.ts index fdb83f07..e666aa23 100644 --- a/source/lib/assertions/jest-like/handlers/subtype-of.ts +++ b/source/lib/assertions/jest-like/handlers/subtype-of.ts @@ -1,10 +1,9 @@ -import {TypeChecker} from '@tsd/typescript'; import {Diagnostic} from '../../../interfaces'; import {makeDiagnostic} from '../../../utils'; -import {JestLikeAssertionNodes} from '..'; +import {JestLikeAssertionNodes, JestLikeContext} from '..'; import {getTypes} from '../util'; -export const subtypeOf = (checker: TypeChecker, nodes: JestLikeAssertionNodes): Diagnostic[] => { +export const subtypeOf = ({typeChecker}: JestLikeContext, nodes: JestLikeAssertionNodes): Diagnostic[] => { const diagnostics: Diagnostic[] = []; if (!nodes) { @@ -14,22 +13,22 @@ export const subtypeOf = (checker: TypeChecker, nodes: JestLikeAssertionNodes): for (const node of nodes) { const [expectedNode, targetNode] = node; - const expected = getTypes(expectedNode, checker); + const expected = getTypes(expectedNode, typeChecker); if (expected.diagnostic) { diagnostics.push(expected.diagnostic); continue; } - const target = getTypes(targetNode, checker); + const target = getTypes(targetNode, typeChecker); if (target.diagnostic) { diagnostics.push(target.diagnostic); continue; } - if (!checker.isTypeSubtypeOf(expected.type, target.type)) { - diagnostics.push(makeDiagnostic(expectedNode, `Expected type \`${checker.typeToString(expected.type)}\` is not a subtype of \`${checker.typeToString(target.type)}\`.`)); + if (!typeChecker.isTypeSubtypeOf(expected.type, target.type)) { + diagnostics.push(makeDiagnostic(expectedNode, `Expected type \`${typeChecker.typeToString(expected.type)}\` is not a subtype of \`${typeChecker.typeToString(target.type)}\`.`)); } } diff --git a/source/lib/assertions/jest-like/handlers/to-throw-error.ts b/source/lib/assertions/jest-like/handlers/to-throw-error.ts new file mode 100644 index 00000000..4678b5d4 --- /dev/null +++ b/source/lib/assertions/jest-like/handlers/to-throw-error.ts @@ -0,0 +1,50 @@ +import {isNumericLiteral, isRegularExpressionLiteral, isStringLiteral} from '@tsd/typescript'; +import {Diagnostic} from '../../../interfaces'; +import {JestLikeAssertionNodes, JestLikeContext, JestLikeExpectedError} from '..'; +import {getTypes, tryToGetTypes} from '../util'; + +export const toThrowError = ({typeChecker, expectedErrors}: JestLikeContext, nodes: JestLikeAssertionNodes): Diagnostic[] => { + const diagnostics: Diagnostic[] = []; + + if (!nodes) { + return diagnostics; + } + + for (const node of nodes) { + const [expectedNode, targetNode] = node; + + const expected = getTypes(expectedNode, typeChecker); + + if (expected.diagnostic) { + diagnostics.push(expected.diagnostic); + continue; + } + + const target = tryToGetTypes(targetNode, typeChecker); + + if (target.diagnostic) { + diagnostics.push(target.diagnostic); + continue; + } + + const start = expected.argument.getStart(); + const end = expected.argument.getEnd(); + + const error: JestLikeExpectedError = {node: expected.argument}; + + if (target.argument) { + if (isStringLiteral(target.argument)) { + error.message = target.argument.text; + } else if (isNumericLiteral(target.argument)) { + error.code = Number(target.argument.text); + } else if (isRegularExpressionLiteral(target.argument)) { + // eslint-disable-next-line no-eval + error.regexp = eval(`new RegExp(${target.argument.text})`) as RegExp; + } + } + + expectedErrors.set({start, end}, error); + } + + return diagnostics; +}; diff --git a/source/lib/assertions/jest-like/index.ts b/source/lib/assertions/jest-like/index.ts index f48ba9eb..becffe07 100644 --- a/source/lib/assertions/jest-like/index.ts +++ b/source/lib/assertions/jest-like/index.ts @@ -1,4 +1,4 @@ -import {CallExpression, TypeChecker} from '@tsd/typescript'; +import {CallExpression, Node, TypeChecker} from '@tsd/typescript'; import {Diagnostic} from '../../interfaces'; import * as handlers from '../jest-like/handlers'; @@ -9,17 +9,25 @@ export type JestLikeHandler = (typeChecker: TypeChecker, nodes: JestLikeAssertio export type JestLikeAssertionHandlers = Map; export type JestLikeAssertions = Map; +export type JestLikeErrorLocation = {start: number; end: number}; +export type JestLikeExpectedError = {node: Node; code?: number; message?: string; regexp?: RegExp}; + +export type JestLikeContext = { + typeChecker: TypeChecker; + assertions: JestLikeAssertions; + expectedErrors: Map; +}; + /** * Returns a list of diagnostics based on the assertions provided. * - * @param typeChecker - The TypeScript type checker. - * @param assertions - Assertion map with the key being the assertion, and the value the list of all those assertion nodes. + * @param context - See {@link JestLikeContext} * @returns List of diagnostics. */ -export const jestLikeHandle = (typeChecker: TypeChecker, assertions: JestLikeAssertions): Diagnostic[] => { +export const jestLikeHandle = (context: JestLikeContext): Diagnostic[] => { const diagnostics: Diagnostic[] = []; - for (const [assertion, nodes] of assertions) { + for (const [assertion, nodes] of context.assertions) { const handler = handlers[assertion]; if (!handler) { @@ -27,7 +35,7 @@ export const jestLikeHandle = (typeChecker: TypeChecker, assertions: JestLikeAss continue; } - diagnostics.push(...handler(typeChecker, nodes)); + diagnostics.push(...handler(context, nodes)); } return diagnostics; diff --git a/source/lib/assertions/jest-like/util.ts b/source/lib/assertions/jest-like/util.ts index d55c7964..0576dd46 100644 --- a/source/lib/assertions/jest-like/util.ts +++ b/source/lib/assertions/jest-like/util.ts @@ -2,11 +2,11 @@ import {CallExpression, Node, Type, TypeChecker} from '@tsd/typescript'; import {Diagnostic} from '../../interfaces'; import {makeDiagnostic} from '../../utils'; -type Types = - | {type: Type; argument: Node; diagnostic?: never} +type MaybeTypes = + | {type: Type | undefined; argument: Node; diagnostic?: never} | {diagnostic: Diagnostic; type?: never; argument?: never}; -export function getTypes(node: CallExpression, checker: TypeChecker): Types { +export function tryToGetTypes(node: CallExpression, checker: TypeChecker): MaybeTypes { let type: Type | undefined; let value: Type | undefined; @@ -29,9 +29,23 @@ export function getTypes(node: CallExpression, checker: TypeChecker): Types { return {type, argument: typeArgument}; } - if (value && valueArgument) { - return {type: value, argument: valueArgument}; + return {type: value, argument: valueArgument}; +} + +type Types = + | {type: Type; argument: Node; diagnostic?: never} + | {diagnostic: Diagnostic; type?: never; argument?: never}; + +export function getTypes(node: CallExpression, checker: TypeChecker): Types { + const {type, argument, diagnostic} = tryToGetTypes(node, checker); + + if (diagnostic) { + return {diagnostic}; + } + + if (!type) { + return {diagnostic: makeDiagnostic(node, 'A generic type or an argument value is required.')}; } - return {diagnostic: makeDiagnostic(node, 'A generic type or an argument value is required.')}; + return {type, argument}; } diff --git a/source/lib/compiler.ts b/source/lib/compiler.ts index 22bb480d..a017f043 100644 --- a/source/lib/compiler.ts +++ b/source/lib/compiler.ts @@ -6,7 +6,8 @@ import { import {ExpectedError, extractAssertions, parseErrorAssertionToLocation} from './parser'; import {Diagnostic, DiagnosticCode, Context, Location} from './interfaces'; import {handle} from './assertions/tsd'; -import {jestLikeHandle} from './assertions/jest-like'; +import {JestLikeContext, JestLikeErrorLocation, JestLikeExpectedError, jestLikeHandle} from './assertions/jest-like'; +import {makeDiagnostic} from './utils'; // List of diagnostic codes that should be ignored in general const ignoredDiagnostics = new Set([ @@ -96,10 +97,17 @@ export const getDiagnostics = (context: Context): Diagnostic[] => { .concat(program.getSyntacticDiagnostics()); const assertions = extractAssertions(program); + const typeChecker = program.getTypeChecker(); + + const jestLikeContext: JestLikeContext = { + typeChecker, + expectedErrors: new Map(), + assertions: assertions.jestLikeAssertions + }; diagnostics.push(...assertions.diagnostics); - diagnostics.push(...handle(program.getTypeChecker(), assertions.assertions)); - diagnostics.push(...jestLikeHandle(program.getTypeChecker(), assertions.jestLikeAssertions)); + diagnostics.push(...jestLikeHandle(jestLikeContext)); + diagnostics.push(...handle(typeChecker, assertions.assertions)); const expectedErrors = parseErrorAssertionToLocation(assertions.assertions); const expectedErrorsLocationsWithFoundDiagnostics: Location[] = []; @@ -122,16 +130,37 @@ export const getDiagnostics = (context: Context): Diagnostic[] => { continue; } - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const position = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start!); + const {fileName} = diagnostic.file; + const start = Number(diagnostic.start); + const position = diagnostic.file.getLineAndCharacterOfPosition(start); + const jestLikeError = findJestLikeErrorAtPosition(jestLikeContext.expectedErrors, start); - diagnostics.push({ - fileName: diagnostic.file.fileName, - message: flattenDiagnosticMessageText(diagnostic.messageText, '\n'), + const pushDiagnostic = (message: string) => diagnostics.push({ + message, + fileName, severity: 'error', line: position.line + 1, column: position.character }); + + if (jestLikeError) { + const [location, error] = jestLikeError; + const message = flattenDiagnosticMessageText(diagnostic.messageText, '\n'); + + jestLikeContext.expectedErrors.delete(location); + + if (error.code && error.code !== diagnostic.code) { + pushDiagnostic(`Expected error with code '${error.code}' but received error with code '${diagnostic.code}'.`); + } else if (error.message && !message.includes(error.message)) { + pushDiagnostic(`Expected error message to includes '${error.message}' but received error with message '${message}'.`); + } else if (error.regexp && !error.regexp.test(message)) { + pushDiagnostic(`Expected error message to match '${error.regexp.source}' but received error with message '${message}'.`); + } + + continue; + } + + pushDiagnostic(flattenDiagnosticMessageText(diagnostic.messageText, '\n')); } for (const errorLocationToRemove of expectedErrorsLocationsWithFoundDiagnostics) { @@ -146,5 +175,19 @@ export const getDiagnostics = (context: Context): Diagnostic[] => { }); } + for (const [, error] of jestLikeContext.expectedErrors) { + diagnostics.push(makeDiagnostic(error.node, 'Expected an error, but found none.')); + } + return diagnostics; }; + +function findJestLikeErrorAtPosition(expectedErrors: JestLikeContext['expectedErrors'], start: number): [JestLikeErrorLocation, JestLikeExpectedError] | undefined { + for (const [location, error] of expectedErrors) { + if (location.start <= start && start <= location.end) { + return [location, error]; + } + } + + return undefined; +} diff --git a/source/test/fixtures/jest-like-api/to-throw-error/index.d.ts b/source/test/fixtures/jest-like-api/to-throw-error/index.d.ts new file mode 100644 index 00000000..e69de29b diff --git a/source/test/fixtures/jest-like-api/to-throw-error/index.js b/source/test/fixtures/jest-like-api/to-throw-error/index.js new file mode 100644 index 00000000..e69de29b diff --git a/source/test/fixtures/jest-like-api/to-throw-error/index.test-d.ts b/source/test/fixtures/jest-like-api/to-throw-error/index.test-d.ts new file mode 100644 index 00000000..9daeca16 --- /dev/null +++ b/source/test/fixtures/jest-like-api/to-throw-error/index.test-d.ts @@ -0,0 +1,15 @@ +import {assertType} from '../../../..'; + +type Test = T; + +// Should pass +assertType>().toThrowError(); +assertType>().toThrowError(2344); +assertType>().toThrowError('does not satisfy the constraint'); +assertType>().toThrowError(/^Type 'string'/); + +// Should fail +assertType>().toThrowError(); +assertType>().toThrowError(2244); +assertType>().toThrowError('poes not satisfy the constraint'); +assertType>().toThrowError(/Type 'string'$/); diff --git a/source/test/fixtures/jest-like-api/to-throw-error/package.json b/source/test/fixtures/jest-like-api/to-throw-error/package.json new file mode 100644 index 00000000..de6dc1db --- /dev/null +++ b/source/test/fixtures/jest-like-api/to-throw-error/package.json @@ -0,0 +1,3 @@ +{ + "name": "foo" +} diff --git a/source/test/jest-like-api.ts b/source/test/jest-like-api.ts index d0c2353b..2149876a 100644 --- a/source/test/jest-like-api.ts +++ b/source/test/jest-like-api.ts @@ -132,6 +132,17 @@ test('not-subtype-of', async t => { ]); }); +test('to-throw-error', async t => { + const diagnostics = await tsd({cwd: path.join(__dirname, 'fixtures/jest-like-api/to-throw-error')}); + + verify(t, diagnostics, [ + [13, 16, 'error', 'Expected error with code \'2244\' but received error with code \'2344\'.'], + [14, 16, 'error', 'Expected error message to includes \'poes not satisfy the constraint\' but received error with message \'Type \'string\' does not satisfy the constraint \'number\'.\'.'], + [15, 16, 'error', 'Expected error message to match \'Type \'string\'$\' but received error with message \'Type \'string\' does not satisfy the constraint \'number\'.\'.'], + [12, 11, 'error', 'Expected an error, but found none.'], + ]); +}); + // // Debug // test('debug', async () => { // const diagnostics = await tsd({cwd: path.join(__dirname, 'fixtures/jest-like-api/identicality')}); diff --git a/source/test/test.ts b/source/test/test.ts index b29f9063..0dc4c2bb 100644 --- a/source/test/test.ts +++ b/source/test/test.ts @@ -430,6 +430,7 @@ test('errors in libs from node_modules are not reported', async t => { const alloweOtherFileFailures = [ /[/\\]lib[/\\]index.d.ts$/, /[/\\]lib[/\\]interfaces.d.ts$/, + /[/\\]lib[/\\]assertions[/\\]jest-like[/\\]api[/\\]to-throw-error.d.ts$/, ]; otherDiagnostics.forEach(diagnostic => { t.true(