diff --git a/readme.md b/readme.md index a3b2520b..9f13621a 100644 --- a/readme.md +++ b/readme.md @@ -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`. diff --git a/source/lib/assertions/assert.ts b/source/lib/assertions/assert.ts index 318c8880..df48f090 100644 --- a/source/lib/assertions/assert.ts +++ b/source/lib/assertions/assert.ts @@ -8,6 +8,17 @@ export const expectType = (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 = (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`. * diff --git a/source/lib/assertions/handlers/identicality.ts b/source/lib/assertions/handlers/identicality.ts new file mode 100644 index 00000000..038211a1 --- /dev/null +++ b/source/lib/assertions/handlers/identicality.ts @@ -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): 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): 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; +}; diff --git a/source/lib/assertions/handlers/index.ts b/source/lib/assertions/handlers/index.ts index 31d18101..fe253d71 100644 --- a/source/lib/assertions/handlers/index.ts +++ b/source/lib/assertions/handlers/index.ts @@ -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'; diff --git a/source/lib/assertions/index.ts b/source/lib/assertions/index.ts index 8a9a700d..2978cd66 100644 --- a/source/lib/assertions/index.ts +++ b/source/lib/assertions/index.ts @@ -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', @@ -14,7 +15,8 @@ export enum Assertion { // List of diagnostic handlers attached to the assertion const assertionHandlers = new Map([ - [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] diff --git a/source/test/fixtures/identicality/identical/index.d.ts b/source/test/fixtures/identicality/identical/index.d.ts new file mode 100644 index 00000000..266914ab --- /dev/null +++ b/source/test/fixtures/identicality/identical/index.d.ts @@ -0,0 +1,6 @@ +declare const concat: { + (foo: string, bar: string): string; + (foo: number, bar: number): number; +}; + +export default concat; diff --git a/source/test/fixtures/identicality/identical/index.js b/source/test/fixtures/identicality/identical/index.js new file mode 100644 index 00000000..f17717f5 --- /dev/null +++ b/source/test/fixtures/identicality/identical/index.js @@ -0,0 +1,3 @@ +module.exports.default = (foo, bar) => { + return foo + bar; +}; diff --git a/source/test/fixtures/identicality/identical/index.test-d.ts b/source/test/fixtures/identicality/identical/index.test-d.ts new file mode 100644 index 00000000..e3950cc3 --- /dev/null +++ b/source/test/fixtures/identicality/identical/index.test-d.ts @@ -0,0 +1,8 @@ +import {expectType} from '../../../..'; +import concat from '.'; + +expectType(concat('foo', 'bar')); +expectType(concat(1, 2)); + +expectType(concat(1, 2)); +expectType(concat('unicorn', 'rainbow')); diff --git a/source/test/fixtures/identicality/identical/package.json b/source/test/fixtures/identicality/identical/package.json new file mode 100644 index 00000000..de6dc1db --- /dev/null +++ b/source/test/fixtures/identicality/identical/package.json @@ -0,0 +1,3 @@ +{ + "name": "foo" +} diff --git a/source/test/fixtures/identicality/not-identical/index.d.ts b/source/test/fixtures/identicality/not-identical/index.d.ts new file mode 100644 index 00000000..266914ab --- /dev/null +++ b/source/test/fixtures/identicality/not-identical/index.d.ts @@ -0,0 +1,6 @@ +declare const concat: { + (foo: string, bar: string): string; + (foo: number, bar: number): number; +}; + +export default concat; diff --git a/source/test/fixtures/identicality/not-identical/index.js b/source/test/fixtures/identicality/not-identical/index.js new file mode 100644 index 00000000..f17717f5 --- /dev/null +++ b/source/test/fixtures/identicality/not-identical/index.js @@ -0,0 +1,3 @@ +module.exports.default = (foo, bar) => { + return foo + bar; +}; diff --git a/source/test/fixtures/identicality/not-identical/index.test-d.ts b/source/test/fixtures/identicality/not-identical/index.test-d.ts new file mode 100644 index 00000000..9dd769df --- /dev/null +++ b/source/test/fixtures/identicality/not-identical/index.test-d.ts @@ -0,0 +1,7 @@ +import {expectNotType} from '../../../..'; +import concat from '.'; + +expectNotType(concat('foo', 'bar')); +expectNotType(concat('foo', 'bar')); + +expectNotType(concat('unicorn', 'rainbow')); diff --git a/source/test/fixtures/identicality/not-identical/package.json b/source/test/fixtures/identicality/not-identical/package.json new file mode 100644 index 00000000..de6dc1db --- /dev/null +++ b/source/test/fixtures/identicality/not-identical/package.json @@ -0,0 +1,3 @@ +{ + "name": "foo" +} diff --git a/source/test/identicality.ts b/source/test/identicality.ts new file mode 100644 index 00000000..5fddad7b --- /dev/null +++ b/source/test/identicality.ts @@ -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`.'] + ]); +});