Skip to content

Commit

Permalink
Add expectNotType assertion - fixes #56
Browse files Browse the repository at this point in the history
  • Loading branch information
SamVerschueren committed Nov 15, 2019
1 parent 67ae75b commit 861db08
Show file tree
Hide file tree
Showing 14 changed files with 165 additions and 3 deletions.
4 changes: 4 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,10 @@ These options will be overridden if a `tsconfig.json` file is found in your proj

Check that the type of `value` is identical to type `T`.

### expectNotType<T>(value)

Check that the type of `value` is not identical to type `T`.

### expectAssignable<T>(value)

Check that the type of `value` is assignable to type `T`.
Expand Down
11 changes: 11 additions & 0 deletions source/lib/assertions/assert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@ export const expectType = <T>(value: T) => { // tslint:disable-line:no-unused
// Do nothing, the TypeScript compiler handles this for us
};

/**
* Check that the type of `value` is not identical to type `T`.
*
* @param value - Value that should be identical to type `T`.
*/
// @ts-ignore
export const expectNotType = <T>(value: any) => { // tslint:disable-line:no-unused
// TODO Use a `not T` type when possible https://github.com/microsoft/TypeScript/pull/29317
// Do nothing, the TypeScript compiler handles this for us
};

/**
* Check that the type of `value` is assignable to type `T`.
*
Expand Down
85 changes: 85 additions & 0 deletions source/lib/assertions/handlers/identicality.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import {CallExpression} from '../../../../libraries/typescript/lib/typescript';
import {TypeChecker} from '../../entities/typescript';
import {Diagnostic} from '../../interfaces';
import {makeDiagnostic} from '../../utils';

/**
* Verifies that the argument of the assertion is identical to the generic type of the assertion.
*
* @param checker - The TypeScript type checker.
* @param nodes - The `expectType` AST nodes.
* @return List of custom diagnostics.
*/
export const isIdentical = (checker: TypeChecker, nodes: Set<CallExpression>): Diagnostic[] => {
const diagnostics: Diagnostic[] = [];

if (!nodes) {
return diagnostics;
}

for (const node of nodes) {
if (!node.typeArguments) {
// Skip if the node does not have generics
continue;
}

// Retrieve the type to be expected. This is the type inside the generic.
const expectedType = checker.getTypeFromTypeNode(node.typeArguments[0]);

// Retrieve the argument type. This is the type to be checked.
const argumentType = checker.getTypeAtLocation(node.arguments[0]);

if (!checker.isTypeAssignableTo(argumentType, expectedType)) {
// The argument type is not assignable to the expected type. TypeScript will catch this for us.
continue;
}

if (!checker.isTypeAssignableTo(expectedType, argumentType)) {
/**
* The expected type is not assignable to the argument type, but the argument type is
* assignable to the expected type. This means our type is too wide.
*/
diagnostics.push(makeDiagnostic(node, `Parameter type \`${checker.typeToString(expectedType)}\` is declared too wide for argument type \`${checker.typeToString(argumentType)}\`.`));
} else if (!checker.isTypeIdenticalTo(expectedType, argumentType)) {
/**
* The expected type and argument type are assignable in both directions. We still have to check
* if the types are identical. See https://github.com/Microsoft/TypeScript/blob/master/doc/spec.md#3.11.2.
*/
diagnostics.push(makeDiagnostic(node, `Parameter type \`${checker.typeToString(expectedType)}\` is not identical to argument type \`${checker.typeToString(argumentType)}\`.`));
}
}

return diagnostics;
};

/**
* Verifies that the argument of the assertion is not identical to the generic type of the assertion.
*
* @param checker - The TypeScript type checker.
* @param nodes - The `expectType` AST nodes.
* @return List of custom diagnostics.
*/
export const isNotIdentical = (checker: TypeChecker, nodes: Set<CallExpression>): Diagnostic[] => {
const diagnostics: Diagnostic[] = [];

if (!nodes) {
return diagnostics;
}

for (const node of nodes) {
if (!node.typeArguments) {
// Skip if the node does not have generics
continue;
}

// Retrieve the type to be expected. This is the type inside the generic.
const expectedType = checker.getTypeFromTypeNode(node.typeArguments[0]);
const argumentType = checker.getTypeAtLocation(node.arguments[0]);

if (checker.isTypeIdenticalTo(expectedType, argumentType)) {
diagnostics.push(makeDiagnostic(node, `Parameter type \`${checker.typeToString(expectedType)}\` is identical to argument type \`${checker.typeToString(argumentType)}\`.`));
}
}

return diagnostics;
};
2 changes: 1 addition & 1 deletion source/lib/assertions/handlers/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export {Handler} from './handler';

// Handlers
export {strictAssertion} from './strict-assertion';
export {isIdentical, isNotIdentical} from './identicality';
export {isNotAssignable} from './assignability';
export {expectDeprecated, expectNotDeprecated} from './expect-deprecated';
6 changes: 4 additions & 2 deletions source/lib/assertions/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import {CallExpression} from '../../../libraries/typescript/lib/typescript';
import {TypeChecker} from '../entities/typescript';
import {Diagnostic} from '../interfaces';
import {Handler, strictAssertion, isNotAssignable, expectDeprecated, expectNotDeprecated} from './handlers';
import {Handler, isIdentical, isNotIdentical, isNotAssignable, expectDeprecated, expectNotDeprecated} from './handlers';

export enum Assertion {
EXPECT_TYPE = 'expectType',
EXPECT_NOT_TYPE = 'expectNotType',
EXPECT_ERROR = 'expectError',
EXPECT_ASSIGNABLE = 'expectAssignable',
EXPECT_NOT_ASSIGNABLE = 'expectNotAssignable',
Expand All @@ -14,7 +15,8 @@ export enum Assertion {

// List of diagnostic handlers attached to the assertion
const assertionHandlers = new Map<string, Handler | Handler[]>([
[Assertion.EXPECT_TYPE, strictAssertion],
[Assertion.EXPECT_TYPE, isIdentical],
[Assertion.EXPECT_NOT_TYPE, isNotIdentical],
[Assertion.EXPECT_NOT_ASSIGNABLE, isNotAssignable],
[Assertion.EXPECT_DEPRECATED, expectDeprecated],
[Assertion.EXPECT_NOT_DEPRECATED, expectNotDeprecated]
Expand Down
6 changes: 6 additions & 0 deletions source/test/fixtures/identicality/identical/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
declare const concat: {
(foo: string, bar: string): string;
(foo: number, bar: number): number;
};

export default concat;
3 changes: 3 additions & 0 deletions source/test/fixtures/identicality/identical/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports.default = (foo, bar) => {
return foo + bar;
};
8 changes: 8 additions & 0 deletions source/test/fixtures/identicality/identical/index.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import {expectType} from '../../../..';
import concat from '.';

expectType<string>(concat('foo', 'bar'));
expectType<number>(concat(1, 2));

expectType<any>(concat(1, 2));
expectType<string | number>(concat('unicorn', 'rainbow'));
3 changes: 3 additions & 0 deletions source/test/fixtures/identicality/identical/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"name": "foo"
}
6 changes: 6 additions & 0 deletions source/test/fixtures/identicality/not-identical/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
declare const concat: {
(foo: string, bar: string): string;
(foo: number, bar: number): number;
};

export default concat;
3 changes: 3 additions & 0 deletions source/test/fixtures/identicality/not-identical/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports.default = (foo, bar) => {
return foo + bar;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import {expectNotType} from '../../../..';
import concat from '.';

expectNotType<number>(concat('foo', 'bar'));
expectNotType<string | number>(concat('foo', 'bar'));

expectNotType<string>(concat('unicorn', 'rainbow'));
3 changes: 3 additions & 0 deletions source/test/fixtures/identicality/not-identical/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"name": "foo"
}
21 changes: 21 additions & 0 deletions source/test/identicality.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import * as path from 'path';
import test from 'ava';
import {verify} from './fixtures/utils';
import tsd from '..';

test('identical', async t => {
const diagnostics = await tsd({cwd: path.join(__dirname, 'fixtures/identicality/identical')});

verify(t, diagnostics, [
[7, 0, 'error', 'Parameter type `any` is declared too wide for argument type `number`.'],
[8, 0, 'error', 'Parameter type `string | number` is declared too wide for argument type `string`.']
]);
});

test('not identical', async t => {
const diagnostics = await tsd({cwd: path.join(__dirname, 'fixtures/identicality/not-identical')});

verify(t, diagnostics, [
[7, 0, 'error', 'Parameter type `string` is identical to argument type `string`.']
]);
});

0 comments on commit 861db08

Please sign in to comment.