Skip to content

Commit

Permalink
feat: tsd expectType
Browse files Browse the repository at this point in the history
  • Loading branch information
skarab42 committed Jul 9, 2022
1 parent 11911b7 commit df55102
Show file tree
Hide file tree
Showing 9 changed files with 206 additions and 15 deletions.
35 changes: 33 additions & 2 deletions src/api/tsd/index.ts
@@ -1,5 +1,36 @@
export const VITE_PLUGIN_VITEST_TYPESCRIPT_ASSERT = 'tsd';

export function expectType() {
return;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type ExplicitAny = any;

export function expectType<T>(value: T) {
return value;
}

export function expectNotType<T>(value: ExplicitAny) {
return value as T;
}

export function expectAssignable<T>(value: T) {
return value;
}

export function expectNotAssignable<T>(value: ExplicitAny) {
return value as T;
}

export function expectError<T = ExplicitAny>(value: T) {
return value;
}

export function expectDeprecated(expression: ExplicitAny) {
return expression as unknown;
}

export function expectNotDeprecated(expression: ExplicitAny) {
return expression as unknown;
}

export function printType(expression: ExplicitAny) {
return expression as unknown;
}
10 changes: 8 additions & 2 deletions src/api/tssert/index.ts
@@ -1,5 +1,11 @@
export const VITE_PLUGIN_VITEST_TYPESCRIPT_ASSERT = 'tssert';

export function expectType() {
return;
export function expectType<SourceType>(source?: unknown) {
const dumbFunction = <TargetType>(target?: unknown) => {
return [source, target] as [SourceType, TargetType];
};

return {
assignableTo: dumbFunction,
};
}
57 changes: 57 additions & 0 deletions src/plugin/api/tsd/assert.ts
@@ -0,0 +1,57 @@
import type ts from 'byots';
import { getTypes } from './util';
import type { AssertionResult } from '../types';
import { createAssertionDiagnostic } from '../../util';

export function expectType(
sourceFile: ts.SourceFile,
node: ts.CallExpression,
checker: ts.TypeChecker,
): AssertionResult {
const { source, target } = getTypes(node, checker);

if (!source.type || !target.type) {
throw new Error('Prout');
}

if (!checker.isTypeAssignableTo(source.type, target.type)) {
const sourceString = checker.typeToString(source.type);
const targetString = checker.typeToString(target.type);

return createAssertionDiagnostic(
`Type \`${sourceString}\` is not assignable to type \`${targetString}\`.`,
sourceFile,
source.position,
);
}

return undefined;
}

// export function expectNotType(): AssertionResult {
// return undefined;
// }

// export function expectAssignable(): AssertionResult {
// return undefined;
// }

// export function expectNotAssignable(): AssertionResult {
// return undefined;
// }

// export function expectError(): AssertionResult {
// return undefined;
// }

// export function expectDeprecated(): AssertionResult {
// return undefined;
// }

// export function expectNotDeprecated(): AssertionResult {
// return undefined;
// }

// export function printType(): AssertionResult {
// return undefined;
// }
20 changes: 17 additions & 3 deletions src/plugin/api/tsd/index.ts
@@ -1,8 +1,22 @@
import type ts from 'byots';
import * as assert from './assert';
import type { ProcessCallExpressionReturn } from '../types';

const names = new Map(Object.entries(assert));

export function processCallExpression(
sourceFile: ts.SourceFile,
node: ts.CallExpression,
checker: ts.TypeChecker,
): ProcessCallExpressionReturn {
let diagnostic: ts.Diagnostic | undefined = undefined;

export function processCallExpression(node: ts.CallExpression) {
const identifier = node.expression.getText();
const assertFunction = names.get(identifier);

if (assertFunction) {
diagnostic = assertFunction(sourceFile, node, checker);
}

// eslint-disable-next-line no-console
console.log('> tsd', { identifier });
return { success: !diagnostic, skipped: !assertFunction, diagnostic };
}
37 changes: 37 additions & 0 deletions src/plugin/api/tsd/util.ts
@@ -0,0 +1,37 @@
import type ts from 'byots';
import { getMiddle } from '../../../typescript/util';

export interface AssertionType {
node: ts.CallExpression;
type: ts.Type | undefined;
position: number;
}

export function getTypes(
node: ts.CallExpression,
checker: ts.TypeChecker,
): {
source: AssertionType;
target: AssertionType;
} {
let targetType = undefined;
let sourceType = undefined;

let targetMiddle = -1;
let sourceMiddle = -1;

if (node.typeArguments?.[0]) {
targetType = checker.getTypeFromTypeNode(node.typeArguments[0]);
targetMiddle = getMiddle(node.typeArguments[0]);
}

if (node.arguments[0]) {
sourceType = checker.getTypeAtLocation(node.arguments[0]);
sourceMiddle = getMiddle(node.arguments[0]);
}

return {
source: { node, type: sourceType, position: sourceMiddle },
target: { node, type: targetType, position: targetMiddle },
};
}
18 changes: 16 additions & 2 deletions src/plugin/api/tssert/index.ts
@@ -1,8 +1,22 @@
import type ts from 'byots';
import { createAssertionDiagnostic } from '../../util';
import type { ProcessCallExpressionReturn } from '../types';

export function processCallExpression(
sourceFile: ts.SourceFile,
node: ts.CallExpression,
checker: ts.TypeChecker,
): ProcessCallExpressionReturn {
let diagnostic: ts.Diagnostic | undefined = undefined;

export function processCallExpression(node: ts.CallExpression) {
const identifier = node.expression.getText();

// eslint-disable-next-line no-console
console.log('> tssert', { identifier });
console.log('> tssert', { identifier, checker, sourceFile });

if (identifier.length < 0) {
diagnostic = createAssertionDiagnostic(`No implemented....`, sourceFile, 0);
}

return { success: !diagnostic, skipped: true, diagnostic };
}
15 changes: 15 additions & 0 deletions src/plugin/api/types.ts
@@ -0,0 +1,15 @@
import type ts from 'byots';

export interface ProcessCallExpressionReturn {
success: boolean;
skipped: boolean;
diagnostic: ts.Diagnostic | undefined;
}

export type AssertionResult = ts.Diagnostic | undefined;

export type AssertionFunction = (
sourceFile: ts.SourceFile,
node: ts.CallExpression,
checker: ts.TypeChecker,
) => AssertionResult;
16 changes: 14 additions & 2 deletions src/plugin/transform.ts
Expand Up @@ -28,7 +28,11 @@ export function transform({ code, fileName, report, typescript }: TransformSetti
}

if (report.includes('type-assertion')) {
typeAssertion(fileName, sourceFile, program, checker);
const diagnostics = typeAssertion(fileName, sourceFile, program, checker);

if (diagnostics.length) {
reportDiagnostics(diagnostics, newCode, fileName);
}
}

return {
Expand All @@ -38,6 +42,8 @@ export function transform({ code, fileName, report, typescript }: TransformSetti
}

function typeAssertion(fileName: string, sourceFile: ts.SourceFile, program: ts.Program, checker: ts.TypeChecker) {
const diagnostics: ts.Diagnostic[] = [];

let apiName: APIName | undefined = undefined;

function visit(node: ts.Node) {
Expand All @@ -55,11 +61,17 @@ function typeAssertion(fileName: string, sourceFile: ts.SourceFile, program: ts.
}

if (apiName && ts.isCallExpression(node)) {
api[apiName].processCallExpression(node);
const result = api[apiName].processCallExpression(sourceFile, node, checker);

if (result.diagnostic) {
diagnostics.push(result.diagnostic);
}
}

node.forEachChild(visit);
}

visit(sourceFile);

return diagnostics;
}
13 changes: 9 additions & 4 deletions test/index.test.ts
Expand Up @@ -4,23 +4,28 @@ import { expectType } from '../src/api/tsd';
// import { expectType } from '../src/api/tssert';

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const prout1 = 'plop';
// const prout1 = 'plop';

test('test-1', () => {
expect('Hello World').toBe(42);

// const prout2 = 'plop';

expectType();
// expectType<string>('hello');
expectType<string>(42);
});

test('test-2', () => {
expectType<string>(42);
// expect('Hello World').toBe(42);
});

describe('describe-1', () => {
// const prout3 = 'plop';

test('test-3', () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const prout4 = 'plop';

// const prout4 = 'plop';
// expect('Hello World').toBe(24);
});
});

0 comments on commit df55102

Please sign in to comment.