diff --git a/source/index.ts b/source/index.ts index ae6aa66e..865dc95c 100644 --- a/source/index.ts +++ b/source/index.ts @@ -1,4 +1,6 @@ import tsd from './lib'; -export * from './lib/assertions/assert'; +export * from './lib/assertions/tsd/assert'; +export * from './lib/assertions/jest-like/assert'; + export default tsd; 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..5d836519 --- /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 expected type is assignable to the type provided in the first generic parameter. + * + * @template TargetType - The target type that will be compared with expected type. + */ +export function assignableTo(): void; + +/** + * Test that the expected type is assignable to the type of the expression provided in the first argument. + * + * @param expression - An expression whose type will be compared with expected type. + */ +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 new file mode 100644 index 00000000..c690fcc2 --- /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 expected type is identical to the type provided in the first generic parameter. + * + * @template TargetType - The target type that will be compared with expected type. + */ +export function identicalTo(): void; + +/** + * Test that the expected type is identical to the type of the expression provided in the first argument. + * + * @param expression - An expression whose type will be compared with expected type. + */ +export function identicalTo(expression: ExpressionType): 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..aebd1dfa --- /dev/null +++ b/source/lib/assertions/jest-like/api/index.ts @@ -0,0 +1,23 @@ +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'; + +import {toThrowError} from './to-throw-error'; + +export const api = { + assignableTo, + identicalTo, + subtypeOf, + toThrowError, + not: { + assignableTo: notAssignableTo, + identicalTo: notIdenticalTo, + subtypeOf: notSubtypeOf + }, +}; + +export type AssertTypeAPI = typeof api; 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..2729ae7d --- /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 expected type is not assignable to the type provided in the first generic parameter. + * + * @template TargetType - The target type that will be compared with expected type. + */ +export function notAssignableTo(): void; + +/** + * Test that the expected type is not assignable to the type of the expression provided in the first argument. + * + * @param expression - An expression whose type will be compared with expected type. + */ +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 new file mode 100644 index 00000000..05ce0bf7 --- /dev/null +++ b/source/lib/assertions/jest-like/api/not-identical-to.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 identical to the type provided in the first generic parameter. + * + * @template TargetType - The target type that will be compared with expected type. + */ +export function notIdenticalTo(): void; + +/** + * Test that the expected type is not identical to the type of the expression provided in the first argument. + * + * @param expression - An expression whose type will be compared with expected type. + */ +export function notIdenticalTo(expression: ExpressionType): void; + +export function notIdenticalTo(): void { } 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/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/assert.ts b/source/lib/assertions/jest-like/assert.ts new file mode 100644 index 00000000..dd0d718a --- /dev/null +++ b/source/lib/assertions/jest-like/assert.ts @@ -0,0 +1,20 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import {api, AssertTypeAPI} from './api'; + +/** + * Create a type assertion holder from the type provided in the first generic parameter. + * + * @template ExpectedType - The expected type that will be compared with another type. + */ +export function assertType(): AssertTypeAPI; + +/** + * Create a type assertion holder from the type provided in the first argument. + * + * @param expression - The expected expression whose type will be compared with another type. + */ +export function assertType(expression: ExpressionType): AssertTypeAPI; + +export function assertType() { + return api; +} diff --git a/source/lib/assertions/jest-like/handlers/assignable-to.ts b/source/lib/assertions/jest-like/handlers/assignable-to.ts new file mode 100644 index 00000000..164e0521 --- /dev/null +++ b/source/lib/assertions/jest-like/handlers/assignable-to.ts @@ -0,0 +1,36 @@ +import {Diagnostic} from '../../../interfaces'; +import {makeDiagnostic} from '../../../utils'; +import {JestLikeAssertionNodes, JestLikeContext} from '..'; +import {getTypes} from '../util'; + +export const assignableTo = ({typeChecker}: 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 = getTypes(targetNode, typeChecker); + + if (target.diagnostic) { + diagnostics.push(target.diagnostic); + continue; + } + + 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)}\`.`)); + } + } + + return diagnostics; +}; diff --git a/source/lib/assertions/jest-like/handlers/identical-to.ts b/source/lib/assertions/jest-like/handlers/identical-to.ts new file mode 100644 index 00000000..acadc3c8 --- /dev/null +++ b/source/lib/assertions/jest-like/handlers/identical-to.ts @@ -0,0 +1,46 @@ +import {Diagnostic} from '../../../interfaces'; +import {makeDiagnostic} from '../../../utils'; +import {JestLikeAssertionNodes, JestLikeContext} from '..'; +import {getTypes} from '../util'; + +export const identicalTo = ({typeChecker}: 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 = getTypes(targetNode, typeChecker); + + if (target.diagnostic) { + diagnostics.push(target.diagnostic); + continue; + } + + 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 (!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 (!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)}\`.`)); + } + } + + return diagnostics; +}; 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..2742f11f --- /dev/null +++ b/source/lib/assertions/jest-like/handlers/index.ts @@ -0,0 +1,9 @@ +export {assignableTo} from './assignable-to'; +export {identicalTo} from './identical-to'; +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 new file mode 100644 index 00000000..68ffb2f4 --- /dev/null +++ b/source/lib/assertions/jest-like/handlers/not-assignable-to.ts @@ -0,0 +1,36 @@ +import {Diagnostic} from '../../../interfaces'; +import {makeDiagnostic} from '../../../utils'; +import {JestLikeAssertionNodes, JestLikeContext} from '..'; +import {getTypes} from '../util'; + +export const notAssignableTo = ({typeChecker}: 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 = getTypes(targetNode, typeChecker); + + if (target.diagnostic) { + diagnostics.push(target.diagnostic); + continue; + } + + 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)}\`.`)); + } + } + + return diagnostics; +}; diff --git a/source/lib/assertions/jest-like/handlers/not-identical-to.ts b/source/lib/assertions/jest-like/handlers/not-identical-to.ts new file mode 100644 index 00000000..6c7a34b5 --- /dev/null +++ b/source/lib/assertions/jest-like/handlers/not-identical-to.ts @@ -0,0 +1,36 @@ +import {Diagnostic} from '../../../interfaces'; +import {makeDiagnostic} from '../../../utils'; +import {JestLikeAssertionNodes, JestLikeContext} from '..'; +import {getTypes} from '../util'; + +export const notIdenticalTo = ({typeChecker}: 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 = getTypes(targetNode, typeChecker); + + if (target.diagnostic) { + diagnostics.push(target.diagnostic); + continue; + } + + 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)}\`.`)); + } + } + + return diagnostics; +}; diff --git a/source/lib/assertions/jest-like/handlers/not-subtype-of.ts b/source/lib/assertions/jest-like/handlers/not-subtype-of.ts new file mode 100644 index 00000000..43e91dde --- /dev/null +++ b/source/lib/assertions/jest-like/handlers/not-subtype-of.ts @@ -0,0 +1,36 @@ +import {Diagnostic} from '../../../interfaces'; +import {makeDiagnostic} from '../../../utils'; +import {JestLikeAssertionNodes, JestLikeContext} from '..'; +import {getTypes} from '../util'; + +export const notSubtypeOf = ({typeChecker}: 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 = getTypes(targetNode, typeChecker); + + if (target.diagnostic) { + diagnostics.push(target.diagnostic); + continue; + } + + 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)}\`.`)); + } + } + + return diagnostics; +}; diff --git a/source/lib/assertions/jest-like/handlers/subtype-of.ts b/source/lib/assertions/jest-like/handlers/subtype-of.ts new file mode 100644 index 00000000..e666aa23 --- /dev/null +++ b/source/lib/assertions/jest-like/handlers/subtype-of.ts @@ -0,0 +1,36 @@ +import {Diagnostic} from '../../../interfaces'; +import {makeDiagnostic} from '../../../utils'; +import {JestLikeAssertionNodes, JestLikeContext} from '..'; +import {getTypes} from '../util'; + +export const subtypeOf = ({typeChecker}: 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 = getTypes(targetNode, typeChecker); + + if (target.diagnostic) { + diagnostics.push(target.diagnostic); + continue; + } + + 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)}\`.`)); + } + } + + return diagnostics; +}; 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 new file mode 100644 index 00000000..becffe07 --- /dev/null +++ b/source/lib/assertions/jest-like/index.ts @@ -0,0 +1,42 @@ +import {CallExpression, Node, TypeChecker} from '@tsd/typescript'; +import {Diagnostic} from '../../interfaces'; + +import * as handlers from '../jest-like/handlers'; + +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 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 context - See {@link JestLikeContext} + * @returns List of diagnostics. + */ +export const jestLikeHandle = (context: JestLikeContext): Diagnostic[] => { + const diagnostics: Diagnostic[] = []; + + for (const [assertion, nodes] of context.assertions) { + const handler = handlers[assertion]; + + if (!handler) { + // Ignore these assertions as no handler is found + continue; + } + + 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 new file mode 100644 index 00000000..0576dd46 --- /dev/null +++ b/source/lib/assertions/jest-like/util.ts @@ -0,0 +1,51 @@ +import {CallExpression, Node, Type, TypeChecker} from '@tsd/typescript'; +import {Diagnostic} from '../../interfaces'; +import {makeDiagnostic} from '../../utils'; + +type MaybeTypes = + | {type: Type | undefined; argument: Node; diagnostic?: never} + | {diagnostic: Diagnostic; type?: never; argument?: never}; + +export function tryToGetTypes(node: CallExpression, checker: TypeChecker): MaybeTypes { + let type: Type | undefined; + let value: Type | undefined; + + const typeArgument = node.typeArguments?.[0]; + const valueArgument = node.arguments[0]; + + if (typeArgument) { + type = checker.getTypeFromTypeNode(typeArgument); + } + + if (valueArgument) { + value = checker.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}; + } + + 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 {type, argument}; +} 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..a017f043 100644 --- a/source/lib/compiler.ts +++ b/source/lib/compiler.ts @@ -5,7 +5,9 @@ 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'; +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([ @@ -95,10 +97,19 @@ export const getDiagnostics = (context: Context): Diagnostic[] => { .concat(program.getSyntacticDiagnostics()); const assertions = extractAssertions(program); + const typeChecker = program.getTypeChecker(); - diagnostics.push(...handle(program.getTypeChecker(), assertions)); + const jestLikeContext: JestLikeContext = { + typeChecker, + expectedErrors: new Map(), + assertions: assertions.jestLikeAssertions + }; - const expectedErrors = parseErrorAssertionToLocation(assertions); + diagnostics.push(...assertions.diagnostics); + diagnostics.push(...jestLikeHandle(jestLikeContext)); + diagnostics.push(...handle(typeChecker, assertions.assertions)); + + const expectedErrors = parseErrorAssertionToLocation(assertions.assertions); const expectedErrorsLocationsWithFoundDiagnostics: Location[] = []; for (const diagnostic of tsDiagnostics) { @@ -119,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) { @@ -143,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/lib/parser.ts b/source/lib/parser.ts index 8ad952f4..49bb34e8 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 {Assertion} from './assertions'; +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.parent as CallExpression]); + + 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.parent as CallExpression]); + + jestLikeAssertions.set(maybeTargetName, nodes); } /** @@ -64,7 +123,7 @@ export const extractAssertions = (program: Program): Map; 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..e3c12fe6 --- /dev/null +++ b/source/test/fixtures/jest-like-api/assignable-to/index.test-d.ts @@ -0,0 +1,65 @@ +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); + +/** +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/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/identical-to/index.d.ts b/source/test/fixtures/jest-like-api/identical-to/index.d.ts new file mode 100644 index 00000000..e69de29b diff --git a/source/test/fixtures/jest-like-api/identical-to/index.js b/source/test/fixtures/jest-like-api/identical-to/index.js new file mode 100644 index 00000000..e69de29b 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..d51d646d --- /dev/null +++ b/source/test/fixtures/jest-like-api/identical-to/index.test-d.ts @@ -0,0 +1,33 @@ +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); + +// 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; + +// 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/identical-to/package.json b/source/test/fixtures/jest-like-api/identical-to/package.json new file mode 100644 index 00000000..de6dc1db --- /dev/null +++ b/source/test/fixtures/jest-like-api/identical-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..94d6f815 --- /dev/null +++ b/source/test/fixtures/jest-like-api/not-assignable-to/index.test-d.ts @@ -0,0 +1,65 @@ +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'); + +/** +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/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/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/not-identical-to/index.test-d.ts b/source/test/fixtures/jest-like-api/not-identical-to/index.test-d.ts new file mode 100644 index 00000000..8de15f6f --- /dev/null +++ b/source/test/fixtures/jest-like-api/not-identical-to/index.test-d.ts @@ -0,0 +1,33 @@ +import {assertType} from '../../../..'; + +declare const fooString: string; +declare const fooNumber: number; + +// Should pass +assertType().not.identicalTo(); +assertType().not.identicalTo(fooString); +assertType(fooString).not.identicalTo(); +assertType(fooString).not.identicalTo(fooNumber); + +// Shoul fail +assertType().not.identicalTo(); +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; + +// 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/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/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/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/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/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 new file mode 100644 index 00000000..2149876a --- /dev/null +++ b/source/test/jest-like-api.ts @@ -0,0 +1,150 @@ +import tsd from '..'; +import test from 'ava'; +import path from 'path'; +import {verify} from './fixtures/utils'; + +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, [ + [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()`.'], + [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('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 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`.'], + ]); +}); + +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', '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`.'], + [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"`.'], + [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`.'], + ]); +}); + +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`.'], + ]); +}); + +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')}); +// console.log(diagnostics); +// }); 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(