Skip to content

Commit c3d0949

Browse files
authoredSep 14, 2022
Add expectDocCommentIncludes assertion (#155)
1 parent 68acb5b commit c3d0949

File tree

16 files changed

+159
-32
lines changed

16 files changed

+159
-32
lines changed
 

‎readme.md

+4
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,10 @@ Asserts that the type and return type of `expression` is `never`.
199199

200200
Useful for checking that all branches are covered.
201201

202+
### expectDocCommentIncludes<T>(expression: any)
203+
204+
Asserts that the documentation comment of `expression` includes string literal type `T`.
205+
202206
## Programmatic API
203207

204208
You can use the programmatic API to retrieve the diagnostics and do something with them. This can be useful to run the tests with AVA, Jest or any other testing framework.

‎source/lib/assertions/assert.ts

+10
Original file line numberDiff line numberDiff line change
@@ -92,3 +92,13 @@ export const expectNever = (expression: never): never => {
9292
export const printType = (expression: any) => {
9393
// Do nothing, the TypeScript compiler handles this for us
9494
};
95+
96+
/**
97+
* Asserts that the documentation comment of `expression` includes string literal type `T`.
98+
*
99+
* @param expression - Expression whose documentation comment should include string literal type `T`.
100+
*/
101+
// @ts-expect-error
102+
export const expectDocCommentIncludes = <T>(expression: any) => {
103+
// Do nothing, the TypeScript compiler handles this for us
104+
};

‎source/lib/assertions/handlers/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@ export {Handler} from './handler';
44
export {isIdentical, isNotIdentical, isNever} from './identicality';
55
export {isNotAssignable} from './assignability';
66
export {expectDeprecated, expectNotDeprecated} from './expect-deprecated';
7-
export {prinTypeWarning} from './informational';
7+
export {printTypeWarning, expectDocCommentIncludes} from './informational';

‎source/lib/assertions/handlers/informational.ts

+51-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {CallExpression, TypeChecker, TypeFormatFlags} from '@tsd/typescript';
22
import {Diagnostic} from '../../interfaces';
3-
import {makeDiagnostic} from '../../utils';
3+
import {makeDiagnostic, tsutils} from '../../utils';
44

55
/**
66
* Default formatting flags set by TS plus the {@link TypeFormatFlags.NoTruncation NoTruncation} flag.
@@ -19,7 +19,7 @@ const typeToStringFormatFlags =
1919
* @param nodes - The `printType` AST nodes.
2020
* @return List of warning diagnostics containing the type of the first argument.
2121
*/
22-
export const prinTypeWarning = (checker: TypeChecker, nodes: Set<CallExpression>): Diagnostic[] => {
22+
export const printTypeWarning = (checker: TypeChecker, nodes: Set<CallExpression>): Diagnostic[] => {
2323
const diagnostics: Diagnostic[] = [];
2424

2525
if (!nodes) {
@@ -36,3 +36,52 @@ export const prinTypeWarning = (checker: TypeChecker, nodes: Set<CallExpression>
3636

3737
return diagnostics;
3838
};
39+
40+
/**
41+
* Asserts that the documentation comment for the argument of the assertion
42+
* includes the string literal generic type of the assertion.
43+
*
44+
* @param checker - The TypeScript type checker.
45+
* @param nodes - The `expectDocCommentIncludes` AST nodes.
46+
* @return List of diagnostics.
47+
*/
48+
export const expectDocCommentIncludes = (checker: TypeChecker, nodes: Set<CallExpression>): Diagnostic[] => {
49+
const diagnostics: Diagnostic[] = [];
50+
51+
if (!nodes) {
52+
return diagnostics;
53+
}
54+
55+
for (const node of nodes) {
56+
const expression = tsutils.expressionToString(checker, node.arguments[0]) ?? '?';
57+
58+
if (!node.typeArguments) {
59+
diagnostics.push(makeDiagnostic(node, `Expected documentation comment for expression \`${expression}\` not specified.`));
60+
continue;
61+
}
62+
63+
const maybeExpectedDocComment = checker.getTypeFromTypeNode(node.typeArguments[0]);
64+
65+
if (!maybeExpectedDocComment.isStringLiteral()) {
66+
diagnostics.push(makeDiagnostic(node, `Expected documentation comment for expression \`${expression}\` should be a string literal.`));
67+
continue;
68+
}
69+
70+
const expectedDocComment = maybeExpectedDocComment.value;
71+
const docComment = tsutils.resolveDocComment(checker, node.arguments[0]);
72+
73+
if (!docComment) {
74+
diagnostics.push(makeDiagnostic(node, `Documentation comment for expression \`${expression}\` not found.`));
75+
continue;
76+
}
77+
78+
if (docComment.includes(expectedDocComment)) {
79+
// Do nothing
80+
continue;
81+
}
82+
83+
diagnostics.push(makeDiagnostic(node, `Documentation comment \`${docComment}\` for expression \`${expression}\` does not include expected \`${expectedDocComment}\`.`));
84+
}
85+
86+
return diagnostics;
87+
};

‎source/lib/assertions/index.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import {
88
expectDeprecated,
99
expectNotDeprecated,
1010
isNever,
11-
prinTypeWarning,
11+
printTypeWarning,
12+
expectDocCommentIncludes,
1213
} from './handlers';
1314

1415
export enum Assertion {
@@ -21,6 +22,7 @@ export enum Assertion {
2122
EXPECT_NOT_DEPRECATED = 'expectNotDeprecated',
2223
EXPECT_NEVER = 'expectNever',
2324
PRINT_TYPE = 'printType',
25+
EXPECT_DOC_COMMENT_INCLUDES = 'expectDocCommentIncludes',
2426
}
2527

2628
// List of diagnostic handlers attached to the assertion
@@ -31,7 +33,8 @@ const assertionHandlers = new Map<Assertion, Handler>([
3133
[Assertion.EXPECT_DEPRECATED, expectDeprecated],
3234
[Assertion.EXPECT_NOT_DEPRECATED, expectNotDeprecated],
3335
[Assertion.EXPECT_NEVER, isNever],
34-
[Assertion.PRINT_TYPE, prinTypeWarning]
36+
[Assertion.PRINT_TYPE, printTypeWarning],
37+
[Assertion.EXPECT_DOC_COMMENT_INCLUDES, expectDocCommentIncludes],
3538
]);
3639

3740
/**

‎source/lib/utils/typescript.ts

+35-11
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,29 @@
1-
import {TypeChecker, Expression, isCallLikeExpression, JSDocTagInfo} from '@tsd/typescript';
1+
import {TypeChecker, Expression, isCallLikeExpression, JSDocTagInfo, displayPartsToString} from '@tsd/typescript';
2+
3+
const resolveCommentHelper = <R extends 'JSDoc' | 'DocComment'>(resolve: R) => {
4+
type ConditionalResolveReturn = (R extends 'JSDoc' ? Map<string, JSDocTagInfo> : string) | undefined;
5+
6+
const handler = (checker: TypeChecker, expression: Expression): ConditionalResolveReturn => {
7+
const ref = isCallLikeExpression(expression) ?
8+
checker.getResolvedSignature(expression) :
9+
checker.getSymbolAtLocation(expression);
10+
11+
if (!ref) {
12+
return;
13+
}
14+
15+
switch (resolve) {
16+
case 'JSDoc':
17+
return new Map<string, JSDocTagInfo>(ref.getJsDocTags().map(tag => [tag.name, tag])) as ConditionalResolveReturn;
18+
case 'DocComment':
19+
return displayPartsToString(ref.getDocumentationComment(checker)) as ConditionalResolveReturn;
20+
default:
21+
return undefined;
22+
}
23+
};
24+
25+
return handler;
26+
};
227

328
/**
429
* Resolve the JSDoc tags from the expression. If these tags couldn't be found, it will return `undefined`.
@@ -7,17 +32,16 @@ import {TypeChecker, Expression, isCallLikeExpression, JSDocTagInfo} from '@tsd/
732
* @param expression - The expression to resolve the JSDoc tags for.
833
* @return A unique Set of JSDoc tags or `undefined` if they couldn't be resolved.
934
*/
10-
export const resolveJSDocTags = (checker: TypeChecker, expression: Expression): Map<string, JSDocTagInfo> | undefined => {
11-
const ref = isCallLikeExpression(expression) ?
12-
checker.getResolvedSignature(expression) :
13-
checker.getSymbolAtLocation(expression);
35+
export const resolveJSDocTags = resolveCommentHelper('JSDoc');
1436

15-
if (!ref) {
16-
return;
17-
}
18-
19-
return new Map<string, JSDocTagInfo>(ref.getJsDocTags().map(tag => [tag.name, tag]));
20-
};
37+
/**
38+
* Resolve the documentation comment from the expression. If the comment can't be found, it will return `undefined`.
39+
*
40+
* @param checker - The TypeScript type checker.
41+
* @param expression - The expression to resolve the documentation comment for.
42+
* @return A string of the documentation comment or `undefined` if it can't be resolved.
43+
*/
44+
export const resolveDocComment = resolveCommentHelper('DocComment');
2145

2246
/**
2347
* Convert a TypeScript expression to a string.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default function (foo: number): number | null;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import {expectDocCommentIncludes} from '../../../..';
2+
3+
const noDocComment = 'no doc comment';
4+
5+
expectDocCommentIncludes<'no doc comment'>(noDocComment);
6+
7+
/** FooBar */
8+
const foo = 'bar';
9+
10+
expectDocCommentIncludes(foo);
11+
expectDocCommentIncludes<boolean>(foo);
12+
expectDocCommentIncludes<'BarFoo'>(foo);
13+
expectDocCommentIncludes<'FooBar'>(foo);
14+
expectDocCommentIncludes<'Foo'>(foo);
15+
expectDocCommentIncludes<'Bar'>(foo);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module.exports.default = foo => {
2+
return foo > 0 ? foo : null;
3+
};

‎source/test/fixtures/print-type/index.test-d.ts renamed to ‎source/test/fixtures/informational/print-type/index.test-d.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {printType} from '../../..';
1+
import {printType} from '../../../..';
22
import {aboveZero, bigType} from '.';
33

44
printType(aboveZero);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"name": "foo"
3+
}

‎source/test/informational.ts

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import path from 'path';
2+
import test from 'ava';
3+
import {verify} from './fixtures/utils';
4+
import tsd from '..';
5+
6+
test('print type', async t => {
7+
const diagnostics = await tsd({cwd: path.join(__dirname, 'fixtures/informational/print-type')});
8+
9+
verify(t, diagnostics, [
10+
[4, 0, 'warning', 'Type for expression `aboveZero` is: `(foo: number) => number | null`'],
11+
[5, 0, 'warning', 'Type for expression `null` is: `null`'],
12+
[6, 0, 'warning', 'Type for expression `undefined` is: `undefined`'],
13+
[7, 0, 'warning', 'Type for expression `null as any` is: `any`'],
14+
[8, 0, 'warning', 'Type for expression `null as never` is: `never`'],
15+
[9, 0, 'warning', 'Type for expression `null as unknown` is: `unknown`'],
16+
[10, 0, 'warning', 'Type for expression `\'foo\'` is: `"foo"`'],
17+
[11, 0, 'warning', 'Type for expression `bigType` is: `{ prop1: SuperTypeWithAnExessiveLongNameThatTakesUpTooMuchSpace; prop2: SuperTypeWithAnExessiveLongNameThatTakesUpTooMuchSpace; prop3: SuperTypeWithAnExessiveLongNameThatTakesUpTooMuchSpace; prop4: SuperTypeWithAnExessiveLongNameThatTakesUpTooMuchSpace; prop5: SuperTypeWithAnExessiveLongNameThatTakesUpTooMuchSpace; prop6: SuperTypeWithAnExessiveLongNameThatTakesUpTooMuchSpace; prop7: SuperTypeWithAnExessiveLongNameThatTakesUpTooMuchSpace; prop8: SuperTypeWithAnExessiveLongNameThatTakesUpTooMuchSpace; prop9: SuperTypeWithAnExessiveLongNameThatTakesUpTooMuchSpace; }`'],
18+
]);
19+
});
20+
21+
test('expect doc comment includes', async t => {
22+
const diagnostics = await tsd({cwd: path.join(__dirname, 'fixtures/informational/expect-doc-comment')});
23+
24+
verify(t, diagnostics, [
25+
[5, 0, 'error', 'Documentation comment for expression `noDocComment` not found.'],
26+
[10, 0, 'error', 'Expected documentation comment for expression `foo` not specified.'],
27+
[11, 0, 'error', 'Expected documentation comment for expression `foo` should be a string literal.'],
28+
[12, 0, 'error', 'Documentation comment `FooBar` for expression `foo` does not include expected `BarFoo`.'],
29+
]);
30+
});

‎source/test/test.ts

-15
Original file line numberDiff line numberDiff line change
@@ -442,21 +442,6 @@ test('allow specifying `rootDir` option in `tsconfig.json`', async t => {
442442
verify(t, diagnostics, []);
443443
});
444444

445-
test('prints the types of expressions passed to `printType` helper', async t => {
446-
const diagnostics = await tsd({cwd: path.join(__dirname, 'fixtures/print-type')});
447-
448-
verify(t, diagnostics, [
449-
[4, 0, 'warning', 'Type for expression `aboveZero` is: `(foo: number) => number | null`'],
450-
[5, 0, 'warning', 'Type for expression `null` is: `null`'],
451-
[6, 0, 'warning', 'Type for expression `undefined` is: `undefined`'],
452-
[7, 0, 'warning', 'Type for expression `null as any` is: `any`'],
453-
[8, 0, 'warning', 'Type for expression `null as never` is: `never`'],
454-
[9, 0, 'warning', 'Type for expression `null as unknown` is: `unknown`'],
455-
[10, 0, 'warning', 'Type for expression `\'foo\'` is: `"foo"`'],
456-
[11, 0, 'warning', 'Type for expression `bigType` is: `{ prop1: SuperTypeWithAnExessiveLongNameThatTakesUpTooMuchSpace; prop2: SuperTypeWithAnExessiveLongNameThatTakesUpTooMuchSpace; prop3: SuperTypeWithAnExessiveLongNameThatTakesUpTooMuchSpace; prop4: SuperTypeWithAnExessiveLongNameThatTakesUpTooMuchSpace; prop5: SuperTypeWithAnExessiveLongNameThatTakesUpTooMuchSpace; prop6: SuperTypeWithAnExessiveLongNameThatTakesUpTooMuchSpace; prop7: SuperTypeWithAnExessiveLongNameThatTakesUpTooMuchSpace; prop8: SuperTypeWithAnExessiveLongNameThatTakesUpTooMuchSpace; prop9: SuperTypeWithAnExessiveLongNameThatTakesUpTooMuchSpace; }`']
457-
]);
458-
});
459-
460445
test('assertions should be identified if imported as an aliased module', async t => {
461446
const diagnostics = await tsd({cwd: path.join(__dirname, 'fixtures/aliased/aliased-module')});
462447

0 commit comments

Comments
 (0)
Please sign in to comment.