Skip to content

Commit

Permalink
feat: tssert toThrowError
Browse files Browse the repository at this point in the history
  • Loading branch information
skarab42 committed Jul 13, 2022
1 parent d00b81f commit efab1f1
Show file tree
Hide file tree
Showing 9 changed files with 193 additions and 9 deletions.
7 changes: 6 additions & 1 deletion src/api/tssert/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ function equalTo<TargetType>(_target?: unknown) {}
function toBeDeprecated<TargetType>(_target?: unknown) {}
function notToBeDeprecated<TargetType>(_target?: unknown) {}

function toThrowError<TargetType = undefined>(_target: unknown = undefined) {}

expectType[API_PROPERTY_KEY] = 'tssert:expectType' as const;

assignableTo[API_PROPERTY_KEY] = 'tssert:assignableTo' as const;
Expand All @@ -29,17 +31,20 @@ notIdenticalTo[API_PROPERTY_KEY] = 'tssert:notIdenticalTo' as const;
subtypeOf[API_PROPERTY_KEY] = 'tssert:subtypeOf' as const;
notSubtypeOf[API_PROPERTY_KEY] = 'tssert:notSubtypeOf' as const;

equalTo[API_PROPERTY_KEY] = 'tssert:equalTo' as const;

toBeDeprecated[API_PROPERTY_KEY] = 'tssert:toBeDeprecated' as const;
notToBeDeprecated[API_PROPERTY_KEY] = 'tssert:notToBeDeprecated' as const;

equalTo[API_PROPERTY_KEY] = 'tssert:equalTo' as const;
toThrowError[API_PROPERTY_KEY] = 'tssert:toThrowError' as const;

const api = {
assignableTo,
identicalTo,
subtypeOf,
equalTo,
toBeDeprecated,
toThrowError,
not: {
assignableTo: notAssignableTo,
identicalTo: notIdenticalTo,
Expand Down
7 changes: 7 additions & 0 deletions src/common/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export enum ErrorCode {
ASSERT_MISSING_METHOD,
ASSERT_MISSING_TYPE_OR_VALUE,
ASSERT_MIXED_TYPE_AND_VALUE,
ASSERT_STRING_OR_NUMBER_LITERAL,

ASSERT_TYPE_NOT_ASSIGNABLE,
ASSERT_TYPE_ASSIGNABLE,
Expand All @@ -20,6 +21,8 @@ export enum ErrorCode {
ASSERT_TYPE_SUBTYPE,

ASSERT_ERROR,
ASSERT_ERROR_CODE,
ASSERT_ERROR_MESSAGE,
ASSERT_DEPRECATED,
ASSERT_NOT_DEPRECATED,
}
Expand All @@ -37,6 +40,7 @@ export const errorMessages: Record<ErrorCode, string> = {
"Missing right side methods, expected something like 'expectType('hello').assignableTo<string>()'.",
[ErrorCode.ASSERT_MISSING_TYPE_OR_VALUE]: 'A type or value is missing.',
[ErrorCode.ASSERT_MIXED_TYPE_AND_VALUE]: 'Do not define a type and value at the same time.',
[ErrorCode.ASSERT_STRING_OR_NUMBER_LITERAL]: 'A literal string or number is expected.',

[ErrorCode.ASSERT_TYPE_NOT_ASSIGNABLE]: "Type '{expected}' is not assignable to type '{argument}'.",
[ErrorCode.ASSERT_TYPE_ASSIGNABLE]: "Type '{expected}' is assignable to type '{argument}'.",
Expand All @@ -47,6 +51,9 @@ export const errorMessages: Record<ErrorCode, string> = {
[ErrorCode.ASSERT_TYPE_SUBTYPE]: "Type '{expected}' is subtype of argument type '{argument}'.",

[ErrorCode.ASSERT_ERROR]: 'An error is expected.',
[ErrorCode.ASSERT_ERROR_CODE]: "An error with code '{expected}' is expected, received '{received}'.",
[ErrorCode.ASSERT_ERROR_MESSAGE]: "An error including the message '{expected}' is expected, received '{received}'.",

[ErrorCode.ASSERT_DEPRECATED]: "Expected '{argument}' to be marked as '@deprecated'.",
[ErrorCode.ASSERT_NOT_DEPRECATED]: "Expected '{argument}' to not be marked as '@deprecated'.",
};
Expand Down
8 changes: 2 additions & 6 deletions src/plugin/assert/tsd/expect-error.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import type ts from 'unleashed-typescript';
import { missingArgument } from '../util';
import type { Assertion } from '../../types';
import type { Compiler } from '../../../typescript/types';
import { createAssertionDiagnostic } from '../../diagnostics';
import { ErrorCode, errorMessage } from '../../../common/error';

function inRange(argument: ts.Node, diagnostic: ts.Diagnostic): boolean {
return !!diagnostic.start && argument.getStart() <= diagnostic.start && diagnostic.start <= argument.getEnd();
}
import { isArgumentInDiagnostic, missingArgument } from '../util';

// https://github.dev/SamVerschueren/tsd/blob/e4a398c1b47a4d2f914446b662840e2be5994997/source/lib/compiler.ts#L54-L55
export function expectError({ node }: Assertion, compiler: Compiler): ts.Diagnostic | undefined {
Expand All @@ -21,7 +17,7 @@ export function expectError({ node }: Assertion, compiler: Compiler): ts.Diagnos
return createAssertionDiagnostic(errorMessage(ErrorCode.ASSERT_ERROR), compiler.sourceFile, argument.getStart());
}

const diagnostic = compiler.diagnostics.find((diagnostic) => inRange(argument, diagnostic));
const diagnostic = compiler.diagnostics.find((diagnostic) => isArgumentInDiagnostic(argument, diagnostic));

if (diagnostic) {
compiler.diagnostics = compiler.diagnostics.filter((d) => d !== diagnostic);
Expand Down
1 change: 1 addition & 0 deletions src/plugin/assert/tssert/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export * from './not-subtype-of';
export * from './equal-to';
export * from './to-be-deprecated';
export * from './not-to-be-deprecated';
export * from './to-throw-error';
66 changes: 66 additions & 0 deletions src/plugin/assert/tssert/to-throw-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { getTypes } from './util';
import ts from 'unleashed-typescript';
import type { Assertion } from '../../types';
import { isArgumentInDiagnostic } from '../util';
import { newLine } from '../../../typescript/util';
import type { Compiler } from '../../../typescript/types';
import { createAssertionDiagnostic } from '../../diagnostics';
import { ErrorCode, errorMessage } from '../../../common/error';

export function toThrowError({ node }: Assertion, compiler: Compiler): ts.Diagnostic | undefined {
const expectedNode = node.expression.getChildAt(0) as ts.CallExpression;
const expected = getTypes(expectedNode, compiler.typeChecker);

if (expected.diagnostic) {
return expected.diagnostic;
}

const argument = getTypes(node, compiler.typeChecker);

if (argument.diagnostic && argument.errorCode !== ErrorCode.ASSERT_MISSING_TYPE_OR_VALUE) {
return argument.diagnostic;
}

if (!compiler.diagnostics.length) {
return createAssertionDiagnostic(errorMessage(ErrorCode.ASSERT_ERROR), compiler.sourceFile, expected.position);
}

const diagnostic = compiler.diagnostics.find((diagnostic) => isArgumentInDiagnostic(expected.argument, diagnostic));

if (!diagnostic) {
return createAssertionDiagnostic(errorMessage(ErrorCode.ASSERT_ERROR), compiler.sourceFile, expected.position);
}

if (argument.argument) {
if (ts.isStringLiteral(argument.argument)) {
const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, newLine);

if (!message.includes(argument.argument.text)) {
return createAssertionDiagnostic(
errorMessage(ErrorCode.ASSERT_ERROR_MESSAGE, { expected: argument.argument.text, received: message }),
compiler.sourceFile,
expected.position,
);
}
} else if (ts.isNumericLiteral(argument.argument)) {
if (Number(argument.argument.text) !== diagnostic.code) {
return createAssertionDiagnostic(
errorMessage(ErrorCode.ASSERT_ERROR_CODE, { expected: argument.argument.text, received: diagnostic.code }),
compiler.sourceFile,
expected.position,
);
}
} else {
// TODO: resolve value
return createAssertionDiagnostic(
errorMessage(ErrorCode.ASSERT_STRING_OR_NUMBER_LITERAL),
compiler.sourceFile,
argument.argument.getStart(),
);
}
}

compiler.diagnostics = compiler.diagnostics.filter((d) => d !== diagnostic);

return;
}
6 changes: 4 additions & 2 deletions src/plugin/assert/tssert/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ export function typeError(
}

type Types =
| { type: ts.Type; position: number; argument: ts.Node; diagnostic?: never }
| { diagnostic: ts.Diagnostic; type?: never; position?: never; argument?: never };
| { type: ts.Type; position: number; argument: ts.Node; errorCode?: never; diagnostic?: never }
| { errorCode: ErrorCode; diagnostic: ts.Diagnostic; type?: never; position?: never; argument?: never };

export function getTypes(node: ts.CallExpression, typeChecker: ts.TypeChecker): Types {
let type = undefined;
Expand All @@ -46,6 +46,7 @@ export function getTypes(node: ts.CallExpression, typeChecker: ts.TypeChecker):

if (type && value) {
return {
errorCode: ErrorCode.ASSERT_MIXED_TYPE_AND_VALUE,
diagnostic: createAssertionDiagnostic(
errorMessage(ErrorCode.ASSERT_MIXED_TYPE_AND_VALUE),
node.getSourceFile(),
Expand All @@ -63,6 +64,7 @@ export function getTypes(node: ts.CallExpression, typeChecker: ts.TypeChecker):
}

return {
errorCode: ErrorCode.ASSERT_MISSING_TYPE_OR_VALUE,
diagnostic: createAssertionDiagnostic(
errorMessage(ErrorCode.ASSERT_MISSING_TYPE_OR_VALUE),
node.getSourceFile(),
Expand Down
4 changes: 4 additions & 0 deletions src/plugin/assert/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,7 @@ export function hasDeprecatedTag(argument: ts.Node, typeChecker: ts.TypeChecker)

return !!tags.find((tag) => tag.name === 'deprecated');
}

export function isArgumentInDiagnostic(argument: ts.Node, diagnostic: ts.Diagnostic): boolean {
return !!diagnostic.start && argument.getStart() <= diagnostic.start && diagnostic.start <= argument.getEnd();
}
81 changes: 81 additions & 0 deletions test/tsd.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { test } from 'vitest';
import * as tsd from '../src/api/tsd';
import { expectType, expectType as assertType } from '../src/api/tsd';

test('test-1', () => {
expectType<string>('hello');
});

test('test-2', () => {
assertType<string>('hello');
});

test('test-3', () => {
tsd.expectNotType<string>('hello');
});

test('test-4', () => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
tsd.expectError(42 === 'life');
});

test('test-5', () => {
tsd.expectError(true);
});

test('test-6', () => {
tsd.expectDeprecated(UnicornClass);
tsd.expectNotDeprecated(UnicornClass);
});

test('test-7', () => {
tsd.printType(prout);
});

test('test-8', () => {
tsd.expectType<typeof prout>({
hello: 'you',
life: 42,
data: {
id: '385643984',
items: [1, 2, 3],
},
});
});

// test('test-9', () => {
// const plop = 42;
// tsd.expectError(true);
// });

// test('test-10', () => {
// const plop = 42;
// });

/**
* @deprecated
*/
export class UnicornClass {
readonly key = '🦄';
}

export class RainbowClass {
readonly key = '🌈';
}

export interface Options {
/**
* @deprecated
*/
readonly separator: string;
readonly delimiter: string;
}

export const prout = {
hello: 'world',
life: 42,
data: {
id: 385643984,
items: [1, 2, 3],
},
};
22 changes: 22 additions & 0 deletions test/tssert.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,28 @@ test('test-9', () => {
expectType<RainbowClass>().not.toBeDeprecated();
});

test('test-4', () => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
expectType(42 === 'life').toThrowError();
});

test('test-5', () => {
expectType(true).toThrowError();
});

test('test-4', () => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
expectType(42 === 'life').toThrowError(2367);

// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
expectType(42 === 'life').toThrowError('Prout');
});

test.only('test-4', () => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
expectType(42 === 'life').toThrowError(42);
});

/**
* @deprecated
*/
Expand Down

0 comments on commit efab1f1

Please sign in to comment.