From df5510260deb40719c20c2a27252e4d5ff537737 Mon Sep 17 00:00:00 2001 From: skarab42 Date: Sat, 9 Jul 2022 13:47:08 +0200 Subject: [PATCH] feat: tsd expectType --- src/api/tsd/index.ts | 35 +++++++++++++++++++-- src/api/tssert/index.ts | 10 ++++-- src/plugin/api/tsd/assert.ts | 57 ++++++++++++++++++++++++++++++++++ src/plugin/api/tsd/index.ts | 20 ++++++++++-- src/plugin/api/tsd/util.ts | 37 ++++++++++++++++++++++ src/plugin/api/tssert/index.ts | 18 +++++++++-- src/plugin/api/types.ts | 15 +++++++++ src/plugin/transform.ts | 16 ++++++++-- test/index.test.ts | 13 +++++--- 9 files changed, 206 insertions(+), 15 deletions(-) create mode 100644 src/plugin/api/tsd/assert.ts create mode 100644 src/plugin/api/tsd/util.ts create mode 100644 src/plugin/api/types.ts diff --git a/src/api/tsd/index.ts b/src/api/tsd/index.ts index 1551682..688d683 100644 --- a/src/api/tsd/index.ts +++ b/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(value: T) { + return value; +} + +export function expectNotType(value: ExplicitAny) { + return value as T; +} + +export function expectAssignable(value: T) { + return value; +} + +export function expectNotAssignable(value: ExplicitAny) { + return value as T; +} + +export function expectError(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; } diff --git a/src/api/tssert/index.ts b/src/api/tssert/index.ts index c8150cf..ea6c2a1 100644 --- a/src/api/tssert/index.ts +++ b/src/api/tssert/index.ts @@ -1,5 +1,11 @@ export const VITE_PLUGIN_VITEST_TYPESCRIPT_ASSERT = 'tssert'; -export function expectType() { - return; +export function expectType(source?: unknown) { + const dumbFunction = (target?: unknown) => { + return [source, target] as [SourceType, TargetType]; + }; + + return { + assignableTo: dumbFunction, + }; } diff --git a/src/plugin/api/tsd/assert.ts b/src/plugin/api/tsd/assert.ts new file mode 100644 index 0000000..b1f634f --- /dev/null +++ b/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; +// } diff --git a/src/plugin/api/tsd/index.ts b/src/plugin/api/tsd/index.ts index 1e3cf16..900fbef 100644 --- a/src/plugin/api/tsd/index.ts +++ b/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 }; } diff --git a/src/plugin/api/tsd/util.ts b/src/plugin/api/tsd/util.ts new file mode 100644 index 0000000..1a80c55 --- /dev/null +++ b/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 }, + }; +} diff --git a/src/plugin/api/tssert/index.ts b/src/plugin/api/tssert/index.ts index 6af4945..a71b45c 100644 --- a/src/plugin/api/tssert/index.ts +++ b/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 }; } diff --git a/src/plugin/api/types.ts b/src/plugin/api/types.ts new file mode 100644 index 0000000..a4d164a --- /dev/null +++ b/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; diff --git a/src/plugin/transform.ts b/src/plugin/transform.ts index 536c5ac..351e5ea 100644 --- a/src/plugin/transform.ts +++ b/src/plugin/transform.ts @@ -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 { @@ -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) { @@ -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; } diff --git a/test/index.test.ts b/test/index.test.ts index ef77406..f2bf604 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -4,14 +4,20 @@ 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('hello'); + expectType(42); +}); + +test('test-2', () => { + expectType(42); + // expect('Hello World').toBe(42); }); describe('describe-1', () => { @@ -19,8 +25,7 @@ describe('describe-1', () => { test('test-3', () => { // eslint-disable-next-line @typescript-eslint/no-unused-vars - const prout4 = 'plop'; - + // const prout4 = 'plop'; // expect('Hello World').toBe(24); }); });