diff --git a/libraries/typescript/lib/typescript.d.ts b/libraries/typescript/lib/typescript.d.ts index 811f3b35..0a900af9 100644 --- a/libraries/typescript/lib/typescript.d.ts +++ b/libraries/typescript/lib/typescript.d.ts @@ -1995,6 +1995,10 @@ declare namespace ts { getAugmentedPropertiesOfType(type: Type): Symbol[]; getRootSymbols(symbol: Symbol): ReadonlyArray; getContextualType(node: Expression): Type | undefined; + /** + * Checks if type `a` is assignable to type `b`. + */ + isAssignableTo(a: Type, b: Type): boolean; /** * returns unknownSignature in the case of an error. * returns undefined if the node is not valid. diff --git a/libraries/typescript/lib/typescript.js b/libraries/typescript/lib/typescript.js index 4dbf0377..fc75c60f 100644 --- a/libraries/typescript/lib/typescript.js +++ b/libraries/typescript/lib/typescript.js @@ -32344,6 +32344,7 @@ var ts; var parsed = ts.getParseTreeNode(node, ts.isFunctionLike); return parsed ? isImplementationOfOverload(parsed) : undefined; }, + isAssignableTo: isTypeAssignableTo, getImmediateAliasedSymbol: getImmediateAliasedSymbol, getAliasedSymbol: resolveAlias, getEmitResolver: getEmitResolver, diff --git a/media/screenshot.png b/media/screenshot.png new file mode 100644 index 00000000..fe59e7b0 Binary files /dev/null and b/media/screenshot.png differ diff --git a/media/strict-assert.png b/media/strict-assert.png new file mode 100644 index 00000000..173f88bd Binary files /dev/null and b/media/strict-assert.png differ diff --git a/package.json b/package.json index 63dc4289..f4913d93 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,8 @@ "dist/index.js", "dist/index.d.ts", "dist/cli.js", - "dist/lib" + "dist/lib", + "libraries" ], "keywords": [ "typescript", @@ -54,6 +55,7 @@ "cpy-cli": "^2.0.0", "del-cli": "^1.1.0", "react": "^16.9.0", + "rxjs": "^6.5.3", "tslint": "^5.11.0", "tslint-xo": "^0.9.0", "typescript": "^3.6.3" diff --git a/readme.md b/readme.md index aab6fb8b..eeaf2846 100644 --- a/readme.md +++ b/readme.md @@ -59,7 +59,23 @@ export default concat; If we don't change the test file and we run the `tsd` command again, the test will fail. - + + +### Strict type assertions + +Type assertions are strict. This means that if you expect the type to be `string | number` but the argument is of type `string`, the tests will fail. + +```ts +import {expectType} from 'tsd'; +import concat from '.'; + +expectType(concat('foo', 'bar')); +expectType(concat('foo', 'bar')); +``` + +If we run `tsd`, we will notice that it reports an error because the `concat` method returns the type `string` and not `string | number`. + + ### Top-level `await` diff --git a/screenshot.png b/screenshot.png deleted file mode 100644 index 64b2721d..00000000 Binary files a/screenshot.png and /dev/null differ diff --git a/source/lib/compiler.ts b/source/lib/compiler.ts index 91e78559..5b1a32fd 100644 --- a/source/lib/compiler.ts +++ b/source/lib/compiler.ts @@ -2,20 +2,24 @@ import * as path from 'path'; import { flattenDiagnosticMessageText, createProgram, - SyntaxKind, Diagnostic as TSDiagnostic, Program, SourceFile, Node, - forEachChild + forEachChild, + isCallExpression, + Identifier, + TypeChecker, + CallExpression } from '../../libraries/typescript'; import {Diagnostic, DiagnosticCode, Context, Location} from './interfaces'; -// List of diagnostic codes that should be ignored +// List of diagnostic codes that should be ignored in general const ignoredDiagnostics = new Set([ DiagnosticCode.AwaitIsOnlyAllowedInAsyncFunction ]); +// List of diagnostic codes which should be ignored inside `expectError` statements const diagnosticCodesToIgnore = new Set([ DiagnosticCode.ArgumentTypeIsNotAssignableToParameterType, DiagnosticCode.PropertyDoesNotExistOnType, @@ -27,30 +31,23 @@ const diagnosticCodesToIgnore = new Set([ ]); /** - * Extract all the `expectError` statements and convert it to a range map. + * Extract all assertions. * - * @param program - The TypeScript program. + * @param program - TypeScript program. */ -const extractExpectErrorRanges = (program: Program) => { - const expectedErrors = new Map>(); +const extractAssertions = (program: Program) => { + const typeAssertions = new Set(); + const errorAssertions = new Set(); function walkNodes(node: Node) { - if (node.kind === SyntaxKind.ExpressionStatement && node.getText().startsWith('expectError')) { - const location = { - fileName: node.getSourceFile().fileName, - start: node.getStart(), - end: node.getEnd() - }; - - const pos = node - .getSourceFile() - .getLineAndCharacterOfPosition(node.getStart()); - - expectedErrors.set(location, { - fileName: location.fileName, - line: pos.line + 1, - column: pos.character - }); + if (isCallExpression(node)) { + const text = (node.expression as Identifier).getText(); + + if (text === 'expectType') { + typeAssertions.add(node); + } else if (text === 'expectError') { + errorAssertions.add(node); + } } forEachChild(node, walkNodes); @@ -60,9 +57,88 @@ const extractExpectErrorRanges = (program: Program) => { walkNodes(sourceFile); } + return { + typeAssertions, + errorAssertions + }; +}; + +/** + * Loop over all the `expectError` nodes and convert them to a range map. + * + * @param nodes - Set of `expectError` nodes. + */ +const extractExpectErrorRanges = (nodes: Set) => { + const expectedErrors = new Map>(); + + // Iterate over the nodes and add the node range to the map + for (const node of nodes) { + const location = { + fileName: node.getSourceFile().fileName, + start: node.getStart(), + end: node.getEnd() + }; + + const pos = node + .getSourceFile() + .getLineAndCharacterOfPosition(node.getStart()); + + expectedErrors.set(location, { + fileName: location.fileName, + line: pos.line + 1, + column: pos.character + }); + } + return expectedErrors; }; +/** + * Assert the expected type from `expectType` calls with the provided type in the argument. + * Returns a list of custom diagnostics. + * + * @param checker - The TypeScript type checker. + * @param nodes - The `expectType` AST nodes. + * @return List of custom diagnostics. + */ +const assertTypes = (checker: TypeChecker, nodes: Set): Diagnostic[] => { + const diagnostics: Diagnostic[] = []; + + 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.isAssignableTo(argumentType, expectedType)) { + // The argument type is not assignable to the expected type. TypeScript will catch this for us. + continue; + } + + if (!checker.isAssignableTo(expectedType, argumentType)) { // tslint:disable-line:early-exit + /** + * At this point, 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. + */ + const position = node.getSourceFile().getLineAndCharacterOfPosition(node.getStart()); + + diagnostics.push({ + fileName: node.getSourceFile().fileName, + message: `Parameter type \`${checker.typeToString(expectedType)}\` is declared too wide for argument type \`${checker.typeToString(argumentType)}\`.`, + severity: 'error', + line: position.line + 1, + column: position.character, + }); + } + } + + return diagnostics; +}; + /** * Check if the provided diagnostic should be ignored. * @@ -112,7 +188,11 @@ export const getDiagnostics = (context: Context): Diagnostic[] => { .getSemanticDiagnostics() .concat(program.getSyntacticDiagnostics()); - const expectedErrors = extractExpectErrorRanges(program); + const {typeAssertions, errorAssertions} = extractAssertions(program); + + const expectedErrors = extractExpectErrorRanges(errorAssertions); + + result.push(...assertTypes(program.getTypeChecker(), typeAssertions)); for (const diagnostic of diagnostics) { if (!diagnostic.file || ignoreDiagnostic(diagnostic, expectedErrors)) { diff --git a/source/test/fixtures/missing-import/index.test-d.ts b/source/test/fixtures/missing-import/index.test-d.ts index 362c4616..24061468 100644 --- a/source/test/fixtures/missing-import/index.test-d.ts +++ b/source/test/fixtures/missing-import/index.test-d.ts @@ -3,6 +3,6 @@ import {LiteralUnion} from '.'; type Pet = LiteralUnion<'dog' | 'cat', string>; -expectType('dog'); -expectType('cat'); -expectType('unicorn'); +expectType('dog' as Pet); +expectType('cat' as Pet); +expectType('unicorn' as Pet); diff --git a/source/test/fixtures/strict-types/loose/index.d.ts b/source/test/fixtures/strict-types/loose/index.d.ts new file mode 100644 index 00000000..7bed995c --- /dev/null +++ b/source/test/fixtures/strict-types/loose/index.d.ts @@ -0,0 +1,7 @@ +declare const one: { + (foo: string, bar: string): string; + (foo: number, bar: number): number; + (): T; +}; + +export default one; diff --git a/source/test/fixtures/strict-types/loose/index.js b/source/test/fixtures/strict-types/loose/index.js new file mode 100644 index 00000000..f17717f5 --- /dev/null +++ b/source/test/fixtures/strict-types/loose/index.js @@ -0,0 +1,3 @@ +module.exports.default = (foo, bar) => { + return foo + bar; +}; diff --git a/source/test/fixtures/strict-types/loose/index.test-d.ts b/source/test/fixtures/strict-types/loose/index.test-d.ts new file mode 100644 index 00000000..b70f52b4 --- /dev/null +++ b/source/test/fixtures/strict-types/loose/index.test-d.ts @@ -0,0 +1,30 @@ +import {Observable} from 'rxjs'; +import {expectType} from '../../../..'; +import one from '.'; + +expectType('cat'); + +expectType(one('foo', 'bar')); +expectType(one(1, 2)); + +expectType(new Date('foo')); +expectType>(new Promise(resolve => resolve(1))); +expectType | string>(new Promise(resolve => resolve(1))); + +expectType>(Promise.resolve(1)); + +expectType>( + one>() +); + +expectType | Observable>( + one | Observable>() +); + +abstract class Foo { + abstract unicorn(): T; +} + +expectType> | Foo | Foo>( + one | Foo | Foo | string>>() +); diff --git a/source/test/fixtures/strict-types/loose/package.json b/source/test/fixtures/strict-types/loose/package.json new file mode 100644 index 00000000..8392a5e3 --- /dev/null +++ b/source/test/fixtures/strict-types/loose/package.json @@ -0,0 +1,6 @@ +{ + "name": "foo", + "dependencies": { + "rxjs": "^6.5.3" + } +} diff --git a/source/test/fixtures/strict-types/strict/index.d.ts b/source/test/fixtures/strict-types/strict/index.d.ts new file mode 100644 index 00000000..7bed995c --- /dev/null +++ b/source/test/fixtures/strict-types/strict/index.d.ts @@ -0,0 +1,7 @@ +declare const one: { + (foo: string, bar: string): string; + (foo: number, bar: number): number; + (): T; +}; + +export default one; diff --git a/source/test/fixtures/strict-types/strict/index.js b/source/test/fixtures/strict-types/strict/index.js new file mode 100644 index 00000000..f17717f5 --- /dev/null +++ b/source/test/fixtures/strict-types/strict/index.js @@ -0,0 +1,3 @@ +module.exports.default = (foo, bar) => { + return foo + bar; +}; diff --git a/source/test/fixtures/strict-types/strict/index.test-d.ts b/source/test/fixtures/strict-types/strict/index.test-d.ts new file mode 100644 index 00000000..a74aa46d --- /dev/null +++ b/source/test/fixtures/strict-types/strict/index.test-d.ts @@ -0,0 +1,26 @@ +import {Observable} from 'rxjs'; +import {expectType} from '../../../..'; +import one from '.'; + +abstract class Foo { + abstract unicorn(): T; +} + +expectType(one('foo', 'bar')); +expectType(one(1, 2)); + +expectType(new Date('foo')); +expectType>(new Promise(resolve => resolve(1))); +expectType>(new Promise(resolve => resolve(1))); + +expectType>(Promise.resolve(1)); + +expectType>(one>()); + +expectType | Observable | Observable>( + one | Observable | Observable>() +); + +expectType> | Foo | Foo>( + one | Foo | Foo | string>>() +); diff --git a/source/test/fixtures/strict-types/strict/package.json b/source/test/fixtures/strict-types/strict/package.json new file mode 100644 index 00000000..8392a5e3 --- /dev/null +++ b/source/test/fixtures/strict-types/strict/package.json @@ -0,0 +1,6 @@ +{ + "name": "foo", + "dependencies": { + "rxjs": "^6.5.3" + } +} diff --git a/source/test/test.ts b/source/test/test.ts index 12664087..730be2dc 100644 --- a/source/test/test.ts +++ b/source/test/test.ts @@ -1,6 +1,35 @@ import * as path from 'path'; -import test from 'ava'; +import test, {ExecutionContext} from 'ava'; import m from '../lib'; +import {Diagnostic} from '../lib/interfaces'; + +type Expectation = [number, number, 'error' | 'warning', string, (string | RegExp)?]; + +/** + * Verify a list of diagnostics. + * + * @param t - The AVA execution context. + * @param diagnostics - List of diagnostics to verify. + * @param expectations - Expected diagnostics. + */ +const verify = (t: ExecutionContext, diagnostics: Diagnostic[], expectations: Expectation[]) => { + t.true(diagnostics.length === expectations.length); + + for (const [index, diagnostic] of diagnostics.entries()) { + t.is(diagnostic.line, expectations[index][0]); + t.is(diagnostic.column, expectations[index][1]); + t.is(diagnostic.severity, expectations[index][2]); + t.is(diagnostic.message, expectations[index][3]); + + const filename = expectations[index][4]; + + if (typeof filename === 'string') { + t.is(diagnostic.fileName, filename); + } else if (typeof filename === 'object') { + t.regex(diagnostic.fileName, filename); + } + } +}; test('throw if no type definition was found', async t => { await t.throwsAsync(m({cwd: path.join(__dirname, 'fixtures/no-tsd')}), 'The type definition `index.d.ts` does not exist. Create one and try again.'); @@ -13,47 +42,33 @@ test('throw if no test is found', async t => { test('return diagnostics', async t => { const diagnostics = await m({cwd: path.join(__dirname, 'fixtures/failure')}); - t.true(diagnostics.length === 1); - t.is(diagnostics[0].message, 'Argument of type \'number\' is not assignable to parameter of type \'string\'.'); + verify(t, diagnostics, [ + [5, 19, 'error', 'Argument of type \'number\' is not assignable to parameter of type \'string\'.'] + ]); }); -test('return diagnostics also from imported files', async t => { +test('return diagnostics from imported files as well', async t => { const diagnostics = await m({cwd: path.join(__dirname, 'fixtures/failure-nested')}); - t.true(diagnostics.length === 2); - - t.is(diagnostics[0].message, 'Argument of type \'number\' is not assignable to parameter of type \'string\'.'); - t.is(path.basename(diagnostics[0].fileName), 'child.test-d.ts'); - - t.is(diagnostics[1].message, 'Argument of type \'number\' is not assignable to parameter of type \'string\'.'); - t.is(path.basename(diagnostics[1].fileName), 'index.test-d.ts'); + verify(t, diagnostics, [ + [5, 19, 'error', 'Argument of type \'number\' is not assignable to parameter of type \'string\'.', /child.test-d.ts$/], + [6, 19, 'error', 'Argument of type \'number\' is not assignable to parameter of type \'string\'.', /index.test-d.ts$/] + ]); }); test('fail if typings file is not part of `files` list', async t => { const diagnostics = await m({cwd: path.join(__dirname, 'fixtures/no-files')}); - t.deepEqual(diagnostics, [ - { - fileName: 'package.json', - message: 'TypeScript type definition `index.d.ts` is not part of the `files` list.', - severity: 'error', - line: 3, - column: 1 - } + verify(t, diagnostics, [ + [3, 1, 'error', 'TypeScript type definition `index.d.ts` is not part of the `files` list.', 'package.json'], ]); }); test('fail if `typings` property is used instead of `types`', async t => { const diagnostics = await m({cwd: path.join(__dirname, 'fixtures/types-property/typings')}); - t.deepEqual(diagnostics, [ - { - fileName: 'package.json', - message: 'Use property `types` instead of `typings`.', - severity: 'error', - column: 1, - line: 3 - } + verify(t, diagnostics, [ + [3, 1, 'error', 'Use property `types` instead of `typings`.', 'package.json'], ]); }); @@ -62,16 +77,9 @@ test('fail if tests don\'t pass in strict mode', async t => { cwd: path.join(__dirname, 'fixtures/failure-strict-null-checks') }); - t.is(diagnostics.length, 1); - - const {fileName, message, severity, line, column} = diagnostics[0]; - t.true(/failure-strict-null-checks\/index.test-d.ts$/.test(fileName)); - t.is(message, `Argument of type 'number | null' is not assignable to parameter of type 'number'. - Type \'null\' is not assignable to type 'number'.` - ); - t.is(severity, 'error'); - t.is(line, 4); - t.is(column, 19); + verify(t, diagnostics, [ + [4, 19, 'error', 'Argument of type \'number | null\' is not assignable to parameter of type \'number\'.\n Type \'null\' is not assignable to type \'number\'.', /failure-strict-null-checks\/index.test-d.ts$/], + ]); }); test('overridden config defaults to `strict` if `strict` is not explicitly overridden', async t => { @@ -79,58 +87,42 @@ test('overridden config defaults to `strict` if `strict` is not explicitly overr cwd: path.join(__dirname, 'fixtures/strict-null-checks-as-default-config-value') }); - t.is(diagnostics.length, 1); - - const {fileName, message, severity, line, column} = diagnostics[0]; - t.true(/strict-null-checks-as-default-config-value\/index.test-d.ts$/.test(fileName)); - t.is(message, `Argument of type 'number | null' is not assignable to parameter of type 'number'. - Type \'null\' is not assignable to type 'number'.` - ); - t.is(severity, 'error'); - t.is(line, 4); - t.is(column, 19); + verify(t, diagnostics, [ + [4, 19, 'error', 'Argument of type \'number | null\' is not assignable to parameter of type \'number\'.\n Type \'null\' is not assignable to type \'number\'.', /strict-null-checks-as-default-config-value\/index.test-d.ts$/], + ]); }); test('fail if types are used from a lib that was not explicitly specified', async t => { const diagnostics = await m({cwd: path.join(__dirname, 'fixtures/lib-config/failure-missing-lib')}); - t.is(diagnostics.length, 2); - - t.true(/failure-missing-lib\/index.d.ts$/.test(diagnostics[0].fileName)); - t.is(diagnostics[0].message, 'Cannot find name \'Window\'.'); - t.is(diagnostics[0].severity, 'error'); - t.is(diagnostics[0].line, 1); - t.is(diagnostics[0].column, 22); - - t.true(/failure-missing-lib\/index.test-d.ts$/.test(diagnostics[1].fileName)); - t.is(diagnostics[1].message, 'Cannot find name \'Window\'.'); - t.is(diagnostics[1].severity, 'error'); - t.is(diagnostics[1].line, 4); - t.is(diagnostics[1].column, 11); + verify(t, diagnostics, [ + [1, 22, 'error', 'Cannot find name \'Window\'.', /failure-missing-lib\/index.d.ts$/], + [4, 11, 'error', 'Cannot find name \'Window\'.', /failure-missing-lib\/index.test-d.ts$/] + ]); }); test('allow specifying a lib as a triple-slash-reference', async t => { const diagnostics = await m({cwd: path.join(__dirname, 'fixtures/lib-config/lib-as-triple-slash-reference')}); - t.true(diagnostics.length === 0); + verify(t, diagnostics, []); }); test('allow specifying a lib in package.json\'s `tsd` field', async t => { const diagnostics = await m({cwd: path.join(__dirname, 'fixtures/lib-config/lib-from-package-json')}); - t.true(diagnostics.length === 0); + verify(t, diagnostics, []); }); test('allow specifying a lib in tsconfig.json', async t => { const diagnostics = await m({cwd: path.join(__dirname, 'fixtures/lib-config/lib-from-tsconfig-json')}); - t.true(diagnostics.length === 0); + verify(t, diagnostics, []); }); test('a lib option in package.json overrdides a lib option in tsconfig.json', async t => { const diagnostics = await m({cwd: path.join(__dirname, 'fixtures/lib-config/lib-from-package-json-overrides-tsconfig-json')}); - t.true(diagnostics.length === 0); + verify(t, diagnostics, []); }); test('pass in loose mode when strict mode is disabled in settings', async t => { @@ -138,121 +130,119 @@ test('pass in loose mode when strict mode is disabled in settings', async t => { cwd: path.join(__dirname, 'fixtures/non-strict-check-with-config') }); - t.true(diagnostics.length === 0); + verify(t, diagnostics, []); }); test('return no diagnostics', async t => { const diagnostics = await m({cwd: path.join(__dirname, 'fixtures/success')}); - t.true(diagnostics.length === 0); + verify(t, diagnostics, []); }); test('support non-barrel main', async t => { const diagnostics = await m({cwd: path.join(__dirname, 'fixtures/test-non-barrel-main')}); - t.true(diagnostics.length === 0); + verify(t, diagnostics, []); }); test('support non-barrel main using `types` property', async t => { const diagnostics = await m({cwd: path.join(__dirname, 'fixtures/test-non-barrel-main-via-types')}); - t.true(diagnostics.length === 0); + verify(t, diagnostics, []); }); test('support testing in sub-directories', async t => { const diagnostics = await m({cwd: path.join(__dirname, 'fixtures/test-in-subdir')}); - t.true(diagnostics.length === 0); + verify(t, diagnostics, []); }); test('support top-level await', async t => { const diagnostics = await m({cwd: path.join(__dirname, 'fixtures/top-level-await')}); - t.true(diagnostics.length === 0); + verify(t, diagnostics, []); }); test('support default test directory', async t => { const diagnostics = await m({cwd: path.join(__dirname, 'fixtures/test-directory/default')}); - t.true(diagnostics.length === 0); + verify(t, diagnostics, []); }); test('support tsx in subdirectory', async t => { const diagnostics = await m({cwd: path.join(__dirname, 'fixtures/test-directory/tsx')}); - t.true(diagnostics.length === 0); + verify(t, diagnostics, []); }); test('support setting a custom test directory', async t => { const diagnostics = await m({cwd: path.join(__dirname, 'fixtures/test-directory/custom')}); - t.true(diagnostics[0].column === 0); - t.true(diagnostics[0].line === 4); - t.true(diagnostics[0].message === 'Expected an error, but found none.'); - t.true(diagnostics[0].severity === 'error'); + verify(t, diagnostics, [ + [4, 0, 'error', 'Expected an error, but found none.'] + ]); }); test('expectError for functions', async t => { const diagnostics = await m({cwd: path.join(__dirname, 'fixtures/expect-error/functions')}); - t.true(diagnostics.length === 1, `Diagnostics: ${diagnostics.map(d => d.message)}`); - - t.true(diagnostics[0].column === 0); - t.true(diagnostics[0].line === 5); - t.true(diagnostics[0].message === 'Expected an error, but found none.'); - t.true(diagnostics[0].severity === 'error'); + verify(t, diagnostics, [ + [5, 0, 'error', 'Expected an error, but found none.'] + ]); }); test('expectError should not ignore syntactical errors', async t => { const diagnostics = await m({cwd: path.join(__dirname, 'fixtures/expect-error/syntax')}); - t.true(diagnostics.length === 4); - - t.true(diagnostics[0].column === 29); - t.true(diagnostics[0].line === 4); - t.true(diagnostics[0].message === '\')\' expected.'); - t.true(diagnostics[0].severity === 'error'); - - t.true(diagnostics[1].column === 22); - t.true(diagnostics[1].line === 5); - t.true(diagnostics[1].message === '\',\' expected.'); - t.true(diagnostics[1].severity === 'error'); - - t.true(diagnostics[2].column === 0); - t.true(diagnostics[2].line === 4); - t.true(diagnostics[2].message === 'Expected an error, but found none.'); - t.true(diagnostics[2].severity === 'error'); - - t.true(diagnostics[3].column === 0); - t.true(diagnostics[3].line === 5); - t.true(diagnostics[3].message === 'Expected an error, but found none.'); - t.true(diagnostics[3].severity === 'error'); + verify(t, diagnostics, [ + [4, 29, 'error', '\')\' expected.'], + [5, 22, 'error', '\',\' expected.'], + [4, 0, 'error', 'Expected an error, but found none.'], + [5, 0, 'error', 'Expected an error, but found none.'] + ]); }); test('expectError for values', async t => { const diagnostics = await m({cwd: path.join(__dirname, 'fixtures/expect-error/values')}); - t.true(diagnostics.length === 1); - - t.true(diagnostics[0].column === 0); - t.true(diagnostics[0].line === 5); - t.true(diagnostics[0].message === 'Expected an error, but found none.'); - t.true(diagnostics[0].severity === 'error'); + verify(t, diagnostics, [ + [5, 0, 'error', 'Expected an error, but found none.'] + ]); }); test('missing import', async t => { const diagnostics = await m({cwd: path.join(__dirname, 'fixtures/missing-import')}); - t.true(diagnostics.length === 1); - - t.true(diagnostics[0].column === 18); - t.true(diagnostics[0].line === 3); - t.true(diagnostics[0].message === 'Cannot find name \'Primitive\'.'); - t.true(diagnostics[0].severity === 'error'); + verify(t, diagnostics, [ + [3, 18, 'error', 'Cannot find name \'Primitive\'.'] + ]); }); test('tsx', async t => { const diagnostics = await m({cwd: path.join(__dirname, 'fixtures/tsx')}); - t.true(diagnostics.length === 0); + verify(t, diagnostics, []); +}); + +test('loose types', async t => { + const diagnostics = await m({cwd: path.join(__dirname, 'fixtures/strict-types/loose')}); + + verify(t, diagnostics, [ + [5, 0, 'error', 'Parameter type `string` is declared too wide for argument type `"cat"`.'], + [7, 0, 'error', 'Parameter type `string | number` is declared too wide for argument type `string`.'], + [8, 0, 'error', 'Parameter type `string | number` is declared too wide for argument type `number`.'], + [10, 0, 'error', 'Parameter type `string | Date` is declared too wide for argument type `Date`.'], + [11, 0, 'error', 'Parameter type `Promise` is declared too wide for argument type `Promise`.'], + [12, 0, 'error', 'Parameter type `string | Promise` is declared too wide for argument type `Promise`.'], + [14, 0, 'error', 'Parameter type `Promise` is declared too wide for argument type `Promise`.'], + [16, 0, 'error', 'Parameter type `Observable` is declared too wide for argument type `Observable`.'], + [20, 0, 'error', 'Parameter type `Observable | Observable` is declared too wide for argument type `Observable | Observable`.'], + [28, 0, 'error', 'Parameter type `Foo> | Foo | Foo` is declared too wide for argument type `Foo | Foo | Foo>`.'] + ]); +}); + +test('strict types', async t => { + const diagnostics = await m({cwd: path.join(__dirname, 'fixtures/strict-types/strict')}); + + verify(t, diagnostics, []); });