Skip to content

Commit

Permalink
feat: tssert assignableTo and not.assignableTo
Browse files Browse the repository at this point in the history
  • Loading branch information
skarab42 committed Jul 12, 2022
1 parent 510230c commit c534504
Show file tree
Hide file tree
Showing 8 changed files with 192 additions and 118 deletions.
13 changes: 11 additions & 2 deletions src/common/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ export enum ErrorCode {

ASSERT_MISSING_GENERIC,
ASSERT_MISSING_ARGUMENT,
ASSERT_MISSING_METHOD,
ASSERT_MISSING_TYPE_OR_VALUE,
ASSERT_MIXED_TYPE_AND_VALUE,

ASSERT_TYPE_NOT_ASSIGNABLE,
ASSERT_TYPE_ASSIGNABLE,
ASSERT_TYPE_NOT_IDENTICAL,
Expand All @@ -24,8 +28,13 @@ export const errorMessages: Record<ErrorCode, string> = {
[ErrorCode.TSCONFIG_FILE_NOT_READABLE]: 'TS config file not readable or empty. File path: {filePath}',
[ErrorCode.TSCONFIG_FILE_PARSE_ERROR]: 'An error occurred during the parsing of the TS config file. {message}',

[ErrorCode.ASSERT_MISSING_GENERIC]: 'Missing generic at position {position}',
[ErrorCode.ASSERT_MISSING_ARGUMENT]: 'Missing argument at position {position}',
[ErrorCode.ASSERT_MISSING_GENERIC]: 'Missing generic at position {position}.',
[ErrorCode.ASSERT_MISSING_ARGUMENT]: 'Missing argument at position {position}.',
[ErrorCode.ASSERT_MISSING_METHOD]:
"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_TYPE_NOT_ASSIGNABLE]: "Type '{expected}' is not assignable to type '{argument}'.",
[ErrorCode.ASSERT_TYPE_ASSIGNABLE]: "Type '{expected}' is assignable to type '{argument}'.",
[ErrorCode.ASSERT_TYPE_NOT_IDENTICAL]: "Type '{expected}' is not identical to argument type '{argument}'.",
Expand Down
33 changes: 33 additions & 0 deletions src/plugin/assert/tssert/assignable-to.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type ts from 'unleashed-typescript';
import type { Assertion } from '../../types';
import { getTypes, typeError } from './util';
import { ErrorCode } from '../../../common/error';
import type { Compiler } from '../../../typescript/types';

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

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

const argument = getTypes(node, typeChecker);

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

if (!typeChecker.isTypeAssignableTo(argument.type, expected.type)) {
return typeError(
ErrorCode.ASSERT_TYPE_NOT_ASSIGNABLE,
typeChecker,
expected.type,
argument.type,
sourceFile,
expectedNode,
);
}

return;
}
13 changes: 13 additions & 0 deletions src/plugin/assert/tssert/expect-type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import ts from 'unleashed-typescript';
import type { Assertion } from '../../types';
import type { Compiler } from '../../../typescript/types';
import { createAssertionDiagnostic } from '../../diagnostics';
import { ErrorCode, errorMessage } from '../../../common/error';

export function expectType({ node }: Assertion, { sourceFile }: Compiler): ts.Diagnostic | undefined {
if (!ts.isCallExpression(node.parent.parent) && !ts.isCallExpression(node.parent.parent.parent)) {
return createAssertionDiagnostic(errorMessage(ErrorCode.ASSERT_MISSING_METHOD), sourceFile, node.getEnd());
}

return;
}
19 changes: 3 additions & 16 deletions src/plugin/assert/tssert/index.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,3 @@
import type ts from 'unleashed-typescript';
import type { Assertion } from '../../types';
import type { Compiler } from '../../../typescript/types';
import { createAssertionDiagnostic } from '../../diagnostics';

export function expectType(assertion: Assertion, compiler: Compiler): ts.Diagnostic | undefined {
return createAssertionDiagnostic('Not yet implemented.', compiler.sourceFile, assertion.node.getStart());
}

export function assignableTo(assertion: Assertion, compiler: Compiler): ts.Diagnostic | undefined {
return createAssertionDiagnostic('Not yet implemented.', compiler.sourceFile, assertion.node.getStart());
}

export function notAssignableTo(assertion: Assertion, compiler: Compiler): ts.Diagnostic | undefined {
return createAssertionDiagnostic('Not yet implemented.', compiler.sourceFile, assertion.node.getStart());
}
export * from './expect-type';
export * from './assignable-to';
export * from './not-assignable-to';
33 changes: 33 additions & 0 deletions src/plugin/assert/tssert/not-assignable-to.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type ts from 'unleashed-typescript';
import type { Assertion } from '../../types';
import { getTypes, typeError } from './util';
import { ErrorCode } from '../../../common/error';
import type { Compiler } from '../../../typescript/types';

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

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

const argument = getTypes(node, typeChecker);

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

if (typeChecker.isTypeAssignableTo(argument.type, expected.type)) {
return typeError(
ErrorCode.ASSERT_TYPE_ASSIGNABLE,
typeChecker,
expected.type,
argument.type,
sourceFile,
expectedNode,
);
}

return;
}
69 changes: 69 additions & 0 deletions src/plugin/assert/tssert/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import type ts from 'unleashed-typescript';
import { getMiddle } from '../../../typescript/util';
import { createAssertionDiagnostic } from '../../diagnostics';
import { ErrorCode, errorMessage } from '../../../common/error';

export function typeError(
code: ErrorCode,
typeChecker: ts.TypeChecker,
expectedType: ts.Type,
argumentType: ts.Type,
sourceFile: ts.SourceFile,
node: ts.CallExpression,
): ts.Diagnostic | undefined {
return createAssertionDiagnostic(
errorMessage(code, {
expected: typeChecker.typeToString(expectedType),
argument: typeChecker.typeToString(argumentType),
}),
sourceFile,
getMiddle(node.typeArguments?.[0] ?? node),
);
}

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

export function getTypes(node: ts.CallExpression, typeChecker: ts.TypeChecker): Types {
let type = undefined;
let value = undefined;

let middlePosition = -1;

if (node.typeArguments?.[0]) {
type = typeChecker.getTypeFromTypeNode(node.typeArguments[0]);
middlePosition = getMiddle(node.typeArguments[0]);
}

if (node.arguments[0]) {
value = typeChecker.getTypeAtLocation(node.arguments[0]);
middlePosition = getMiddle(node.arguments[0]);
}

if (type && value) {
return {
diagnostic: createAssertionDiagnostic(
errorMessage(ErrorCode.ASSERT_MIXED_TYPE_AND_VALUE),
node.getSourceFile(),
getMiddle(node.typeArguments?.[0] ?? node),
),
};
}

if (type) {
return { type, position: middlePosition };
}

if (value) {
return { type: value, position: middlePosition };
}

return {
diagnostic: createAssertionDiagnostic(
errorMessage(ErrorCode.ASSERT_MISSING_TYPE_OR_VALUE),
node.getSourceFile(),
node.getEnd() - 2,
),
};
}
81 changes: 0 additions & 81 deletions test/tsd.test.ts

This file was deleted.

49 changes: 30 additions & 19 deletions test/tssert.test.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,40 @@
// import { test, expect, describe } from 'vitest';
// import { expectType, expectType as prout } from '../src/api/tssert';
// import * as tssert from '../src/api/tssert';

// describe('describe-1', () => {
// test('test-1', () => {
// prout<string>().assignableTo('hello');
// expectType<string>().assignableTo('hello');
// expectType<number>().not.assignableTo('hello');
// tssert.expectType<number>().not.assignableTo('hello');

// expect(42).toBe(42);
// // expect("life").toBe(42);
// });
// });

import { test } from 'vitest';
import * as tssert from '../src/api/tssert';
import { expectType, expectType as assertType } from '../src/api/tssert';

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

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

test('test-3', () => {
expectType<string>().assignableTo('hello');
expectType<number>().not.assignableTo('hello');
expectType<string>().assignableTo(42);
// expectType<number>().not.assignableTo('hello');
// tssert.expectType<number>().not.assignableTo('hello');
});

test('test-4', () => {
// expectType<number>().not.assignableTo('hello');
tssert.expectType<number>().not.assignableTo('hello');
tssert.expectType<number>().not.assignableTo(42);
});

test('test-5', () => {
expectType<string>('hello').assignableTo('world');
});

test('test-6', () => {
expectType('hello').assignableTo<string>('world');
});

test('test-7', () => {
expectType().assignableTo<string>('world');
});

test('test-8', () => {
expectType('hello').assignableTo();
});

0 comments on commit c534504

Please sign in to comment.