Skip to content

Commit 3549244

Browse files
Add expectError assertion - fixes #2
1 parent 18f86ec commit 3549244

File tree

17 files changed

+208
-9
lines changed

17 files changed

+208
-9
lines changed

readme.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,14 @@ If we don't change the test file and we run the `tsd-check` command again, the t
6666
If your method returns a `Promise`, you can use top-level `await` to resolve the value instead of wrapping it in an `async` [IIFE](https://developer.mozilla.org/en-US/docs/Glossary/IIFE).
6767

6868
```ts
69-
import {expectType} from 'tsd-check';
69+
import {expectType, expectError} from 'tsd-check';
7070
import concat from '.';
7171

7272
expectType<Promise<string>>(concat('foo', 'bar'));
7373

7474
expectType<string>(await concat('foo', 'bar'));
75+
76+
expectError(await concat(true, false));
7577
```
7678

7779

@@ -81,6 +83,14 @@ expectType<string>(await concat('foo', 'bar'));
8183

8284
Check if a value is of a specific type.
8385

86+
### expectError(function)
87+
88+
Check if the function call has argument type errors.
89+
90+
### expectError<T>(value)
91+
92+
Check if a value is of the provided type `T`.
93+
8494

8595
## License
8696

source/lib/assert.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,15 @@
55
*/
66
// @ts-ignore
77
export const expectType = <T>(value: T) => { // tslint:disable-line:no-unused
8-
// Do nothing, the TypeScript compiler handles this for use
8+
// Do nothing, the TypeScript compiler handles this for us
9+
};
10+
11+
/**
12+
* Assert the value to throw an argument error.
13+
*
14+
* @param value - Value that should be checked.
15+
*/
16+
// @ts-ignore
17+
export const expectError = <T = any>(value: T) => { // tslint:disable-line:no-unused
18+
// Do nothing, the TypeScript compiler handles this for us
919
};

source/lib/compiler.ts

Lines changed: 78 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,16 @@ import {
66
flattenDiagnosticMessageText,
77
CompilerOptions,
88
createProgram,
9-
JsxEmit
9+
JsxEmit,
10+
SyntaxKind,
11+
SourceFile,
12+
Diagnostic as TSDiagnostic
1013
} from 'typescript';
11-
import {Diagnostic, Context} from './interfaces';
14+
import {Diagnostic, DiagnosticCode, Context, Position} from './interfaces';
1215

1316
// List of diagnostic codes that should be ignored
1417
const ignoredDiagnostics = new Set<number>([
15-
1308 // Support top-level `await`
18+
DiagnosticCode.AwaitIsOnlyAllowedInAsyncFunction
1619
]);
1720

1821
const loadConfig = (cwd: string): CompilerOptions => {
@@ -36,6 +39,66 @@ const loadConfig = (cwd: string): CompilerOptions => {
3639
};
3740
};
3841

42+
/**
43+
* Extract all the `expectError` statements and convert it to a range map.
44+
*
45+
* @param sourceFile - File to extract the statements from.
46+
*/
47+
const extractExpectErrorRanges = (sourceFile: SourceFile) => {
48+
const expectedErrors = new Map<Position, Pick<Diagnostic, 'fileName' | 'line' | 'column'>>();
49+
50+
for (const statement of sourceFile.statements) {
51+
if (statement.kind !== SyntaxKind.ExpressionStatement || !statement.getText().startsWith('expectError')) {
52+
continue;
53+
}
54+
55+
const position = {
56+
start: statement.getStart(),
57+
end: statement.getEnd()
58+
};
59+
60+
const pos = statement.getSourceFile().getLineAndCharacterOfPosition(statement.getStart());
61+
62+
expectedErrors.set(position, {
63+
fileName: statement.getSourceFile().fileName,
64+
line: pos.line + 1,
65+
column: pos.character
66+
});
67+
}
68+
69+
return expectedErrors;
70+
};
71+
72+
/**
73+
* Check if the provided diagnostic should be ignored.
74+
*
75+
* @param diagnostic - The diagnostic to validate.
76+
* @param expectedErrors - Map of the expected errors.
77+
* @return Boolean indicating if the diagnostic should be ignored or not.
78+
*/
79+
const ignoreDiagnostic = (diagnostic: TSDiagnostic, expectedErrors: Map<Position, any>): boolean => {
80+
if (ignoredDiagnostics.has(diagnostic.code)) {
81+
// Filter out diagnostics which are present in the `ignoredDiagnostics` set
82+
return true;
83+
}
84+
85+
if (diagnostic.code !== DiagnosticCode.ArgumentTypeIsNotAssignableToParameterType) {
86+
return false;
87+
}
88+
89+
for (const [range] of expectedErrors) {
90+
const start = diagnostic.start as number;
91+
92+
if (start > range.start && start < range.end) {
93+
// Remove the expected error from the Map so it's not being reported as failure
94+
expectedErrors.delete(range);
95+
return true;
96+
}
97+
}
98+
99+
return false;
100+
};
101+
39102
/**
40103
* Get a list of TypeScript diagnostics within the current context.
41104
*
@@ -55,14 +118,14 @@ export const getDiagnostics = (context: Context): Diagnostic[] => {
55118
.getSemanticDiagnostics()
56119
.concat(program.getSyntacticDiagnostics());
57120

121+
const expectedErrors = extractExpectErrorRanges(program.getSourceFile(fileName) as SourceFile);
122+
58123
for (const diagnostic of diagnostics) {
59-
if (!diagnostic.file || ignoredDiagnostics.has(diagnostic.code)) {
124+
if (!diagnostic.file || ignoreDiagnostic(diagnostic, expectedErrors)) {
60125
continue;
61126
}
62127

63-
const position = diagnostic.file.getLineAndCharacterOfPosition(
64-
diagnostic.start as number
65-
);
128+
const position = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start as number);
66129

67130
result.push({
68131
fileName: diagnostic.file.fileName,
@@ -73,5 +136,13 @@ export const getDiagnostics = (context: Context): Diagnostic[] => {
73136
});
74137
}
75138

139+
for (const [, diagnostic] of expectedErrors) {
140+
result.push({
141+
...diagnostic,
142+
message: 'Expected an error, but found none.',
143+
severity: 'error'
144+
});
145+
}
146+
76147
return result;
77148
};

source/lib/interfaces.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,20 @@ export interface Context {
55
testFile: string;
66
}
77

8+
export enum DiagnosticCode {
9+
AwaitIsOnlyAllowedInAsyncFunction = 1308,
10+
ArgumentTypeIsNotAssignableToParameterType = 2345
11+
}
12+
813
export interface Diagnostic {
914
fileName: string;
1015
message: string;
1116
severity: 'error' | 'warning';
1217
line?: number;
1318
column?: number;
1419
}
20+
21+
export interface Position {
22+
start: number;
23+
end: number;
24+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
declare const one: {
2+
(foo: string, bar: string): string;
3+
(foo: number, bar: number): number;
4+
};
5+
6+
export default one;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module.exports.default = (foo, bar) => {
2+
return foo + bar;
3+
};
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import {expectError} from '../../../..';
2+
import one from '.';
3+
4+
expectError(one(true, true));
5+
expectError(one('foo', 'bar'));
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"name": "foo"
3+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
declare const one: {
2+
(foo: string, bar: string): string;
3+
(foo: number, bar: number): number;
4+
};
5+
6+
export default one;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module.exports.default = (foo, bar) => {
2+
return foo + bar;
3+
};
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import {expectError} from '../../../..';
2+
import one from '.';
3+
4+
expectError(one('foo', 'bar');
5+
expectError(one('foo' 'bar'));
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"name": "foo"
3+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
declare const one: {
2+
(foo: string, bar: string): string;
3+
(foo: number, bar: number): number;
4+
};
5+
6+
export default one;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module.exports.default = (foo, bar) => {
2+
return foo + bar;
3+
};
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import {expectError} from '../../../..';
2+
3+
expectError<string>(1);
4+
expectError<string>('fo');
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"name": "foo"
3+
}

source/test/test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,51 @@ test('support top-level await', async t => {
9797

9898
t.true(diagnostics.length === 0);
9999
});
100+
101+
test('expectError for functions', async t => {
102+
const diagnostics = await m({cwd: path.join(__dirname, 'fixtures/expect-error/functions')});
103+
104+
t.true(diagnostics.length === 1);
105+
106+
t.true(diagnostics[0].column === 0);
107+
t.true(diagnostics[0].line === 5);
108+
t.true(diagnostics[0].message === 'Expected an error, but found none.');
109+
t.true(diagnostics[0].severity === 'error');
110+
});
111+
112+
test('expectError should not ignore syntactical errors', async t => {
113+
const diagnostics = await m({cwd: path.join(__dirname, 'fixtures/expect-error/syntax')});
114+
115+
t.true(diagnostics.length === 4);
116+
117+
t.true(diagnostics[0].column === 29);
118+
t.true(diagnostics[0].line === 4);
119+
t.true(diagnostics[0].message === '\')\' expected.');
120+
t.true(diagnostics[0].severity === 'error');
121+
122+
t.true(diagnostics[1].column === 22);
123+
t.true(diagnostics[1].line === 5);
124+
t.true(diagnostics[1].message === '\',\' expected.');
125+
t.true(diagnostics[1].severity === 'error');
126+
127+
t.true(diagnostics[2].column === 0);
128+
t.true(diagnostics[2].line === 4);
129+
t.true(diagnostics[2].message === 'Expected an error, but found none.');
130+
t.true(diagnostics[2].severity === 'error');
131+
132+
t.true(diagnostics[3].column === 0);
133+
t.true(diagnostics[3].line === 5);
134+
t.true(diagnostics[3].message === 'Expected an error, but found none.');
135+
t.true(diagnostics[3].severity === 'error');
136+
});
137+
138+
test('expectError for values', async t => {
139+
const diagnostics = await m({cwd: path.join(__dirname, 'fixtures/expect-error/values')});
140+
141+
t.true(diagnostics.length === 1);
142+
143+
t.true(diagnostics[0].column === 0);
144+
t.true(diagnostics[0].line === 4);
145+
t.true(diagnostics[0].message === 'Expected an error, but found none.');
146+
t.true(diagnostics[0].severity === 'error');
147+
});

0 commit comments

Comments
 (0)