From 68acb5be2640d05f0ad854c1684cae1136f89d76 Mon Sep 17 00:00:00 2001 From: skarab42 Date: Tue, 13 Sep 2022 19:30:50 +0200 Subject: [PATCH 1/2] Don't truncate output of `printType` (#154) --- .../lib/assertions/handlers/informational.ts | 15 +++++++++++++-- source/test/fixtures/print-type/index.d.ts | 18 +++++++++++++++++- .../test/fixtures/print-type/index.test-d.ts | 3 ++- source/test/test.ts | 1 + 4 files changed, 33 insertions(+), 4 deletions(-) diff --git a/source/lib/assertions/handlers/informational.ts b/source/lib/assertions/handlers/informational.ts index 493d6efc..95846cb6 100644 --- a/source/lib/assertions/handlers/informational.ts +++ b/source/lib/assertions/handlers/informational.ts @@ -1,7 +1,17 @@ -import {CallExpression, TypeChecker} from '@tsd/typescript'; +import {CallExpression, TypeChecker, TypeFormatFlags} from '@tsd/typescript'; import {Diagnostic} from '../../interfaces'; import {makeDiagnostic} from '../../utils'; +/** + * Default formatting flags set by TS plus the {@link TypeFormatFlags.NoTruncation NoTruncation} flag. + * + * @see {@link https://github.dev/microsoft/TypeScript/blob/b975dfa1027d1f3073fa7cbe6f7045bf4c882785/src/compiler/checker.ts#L4717 TypeChecker.typeToString} + */ +const typeToStringFormatFlags = + TypeFormatFlags.AllowUniqueESSymbolType | + TypeFormatFlags.UseAliasDefinedOutsideCurrentScope | + TypeFormatFlags.NoTruncation; + /** * Prints the type of the argument of the assertion as a warning. * @@ -19,8 +29,9 @@ export const prinTypeWarning = (checker: TypeChecker, nodes: Set for (const node of nodes) { const argumentType = checker.getTypeAtLocation(node.arguments[0]); const argumentExpression = node.arguments[0].getText(); + const typeString = checker.typeToString(argumentType, node, typeToStringFormatFlags); - diagnostics.push(makeDiagnostic(node, `Type for expression \`${argumentExpression}\` is: \`${checker.typeToString(argumentType)}\``, 'warning')); + diagnostics.push(makeDiagnostic(node, `Type for expression \`${argumentExpression}\` is: \`${typeString}\``, 'warning')); } return diagnostics; diff --git a/source/test/fixtures/print-type/index.d.ts b/source/test/fixtures/print-type/index.d.ts index 6f3a1c51..12ee5dfd 100644 --- a/source/test/fixtures/print-type/index.d.ts +++ b/source/test/fixtures/print-type/index.d.ts @@ -1 +1,17 @@ -export default function (foo: number): number | null; +export function aboveZero(foo: number): number | null; + +type SuperTypeWithAnExessiveLongNameThatTakesUpTooMuchSpace = { + life: 42 +}; + +export const bigType: { + prop1: SuperTypeWithAnExessiveLongNameThatTakesUpTooMuchSpace; + prop2: SuperTypeWithAnExessiveLongNameThatTakesUpTooMuchSpace; + prop3: SuperTypeWithAnExessiveLongNameThatTakesUpTooMuchSpace; + prop4: SuperTypeWithAnExessiveLongNameThatTakesUpTooMuchSpace; + prop5: SuperTypeWithAnExessiveLongNameThatTakesUpTooMuchSpace; + prop6: SuperTypeWithAnExessiveLongNameThatTakesUpTooMuchSpace; + prop7: SuperTypeWithAnExessiveLongNameThatTakesUpTooMuchSpace; + prop8: SuperTypeWithAnExessiveLongNameThatTakesUpTooMuchSpace; + prop9: SuperTypeWithAnExessiveLongNameThatTakesUpTooMuchSpace; +} diff --git a/source/test/fixtures/print-type/index.test-d.ts b/source/test/fixtures/print-type/index.test-d.ts index 9a17b216..55ac5119 100644 --- a/source/test/fixtures/print-type/index.test-d.ts +++ b/source/test/fixtures/print-type/index.test-d.ts @@ -1,5 +1,5 @@ import {printType} from '../../..'; -import aboveZero from '.'; +import {aboveZero, bigType} from '.'; printType(aboveZero); printType(null); @@ -8,3 +8,4 @@ printType(null as any); printType(null as never); printType(null as unknown); printType('foo'); +printType(bigType); diff --git a/source/test/test.ts b/source/test/test.ts index 2600df8a..3529dd0f 100644 --- a/source/test/test.ts +++ b/source/test/test.ts @@ -453,6 +453,7 @@ test('prints the types of expressions passed to `printType` helper', async t => [8, 0, 'warning', 'Type for expression `null as never` is: `never`'], [9, 0, 'warning', 'Type for expression `null as unknown` is: `unknown`'], [10, 0, 'warning', 'Type for expression `\'foo\'` is: `"foo"`'], + [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; }`'] ]); }); From c3d0949e2aaa56160a37bd39a0d208dac53c5a0d Mon Sep 17 00:00:00 2001 From: Tommy Date: Wed, 14 Sep 2022 01:35:31 -0500 Subject: [PATCH 2/2] Add `expectDocCommentIncludes` assertion (#155) --- readme.md | 4 ++ source/lib/assertions/assert.ts | 10 ++++ source/lib/assertions/handlers/index.ts | 2 +- .../lib/assertions/handlers/informational.ts | 53 ++++++++++++++++++- source/lib/assertions/index.ts | 7 ++- source/lib/utils/typescript.ts | 46 ++++++++++++---- .../expect-doc-comment/index.d.ts | 1 + .../expect-doc-comment}/index.js | 0 .../expect-doc-comment/index.test-d.ts | 15 ++++++ .../expect-doc-comment}/package.json | 0 .../{ => informational}/print-type/index.d.ts | 0 .../informational/print-type/index.js | 3 ++ .../print-type/index.test-d.ts | 2 +- .../informational/print-type/package.json | 3 ++ source/test/informational.ts | 30 +++++++++++ source/test/test.ts | 15 ------ 16 files changed, 159 insertions(+), 32 deletions(-) create mode 100644 source/test/fixtures/informational/expect-doc-comment/index.d.ts rename source/test/fixtures/{print-type => informational/expect-doc-comment}/index.js (100%) create mode 100644 source/test/fixtures/informational/expect-doc-comment/index.test-d.ts rename source/test/fixtures/{print-type => informational/expect-doc-comment}/package.json (100%) rename source/test/fixtures/{ => informational}/print-type/index.d.ts (100%) create mode 100644 source/test/fixtures/informational/print-type/index.js rename source/test/fixtures/{ => informational}/print-type/index.test-d.ts (84%) create mode 100644 source/test/fixtures/informational/print-type/package.json create mode 100644 source/test/informational.ts diff --git a/readme.md b/readme.md index 989d9f24..0fff1e9f 100644 --- a/readme.md +++ b/readme.md @@ -199,6 +199,10 @@ Asserts that the type and return type of `expression` is `never`. Useful for checking that all branches are covered. +### expectDocCommentIncludes<T>(expression: any) + +Asserts that the documentation comment of `expression` includes string literal type `T`. + ## Programmatic API 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. diff --git a/source/lib/assertions/assert.ts b/source/lib/assertions/assert.ts index a9454850..88f4c863 100644 --- a/source/lib/assertions/assert.ts +++ b/source/lib/assertions/assert.ts @@ -92,3 +92,13 @@ export const expectNever = (expression: never): never => { export const printType = (expression: any) => { // Do nothing, the TypeScript compiler handles this for us }; + +/** + * Asserts that the documentation comment of `expression` includes string literal type `T`. + * + * @param expression - Expression whose documentation comment should include string literal type `T`. + */ +// @ts-expect-error +export const expectDocCommentIncludes = (expression: any) => { + // Do nothing, the TypeScript compiler handles this for us +}; diff --git a/source/lib/assertions/handlers/index.ts b/source/lib/assertions/handlers/index.ts index 01b3a47f..8386e765 100644 --- a/source/lib/assertions/handlers/index.ts +++ b/source/lib/assertions/handlers/index.ts @@ -4,4 +4,4 @@ export {Handler} from './handler'; export {isIdentical, isNotIdentical, isNever} from './identicality'; export {isNotAssignable} from './assignability'; export {expectDeprecated, expectNotDeprecated} from './expect-deprecated'; -export {prinTypeWarning} from './informational'; +export {printTypeWarning, expectDocCommentIncludes} from './informational'; diff --git a/source/lib/assertions/handlers/informational.ts b/source/lib/assertions/handlers/informational.ts index 95846cb6..a7f08f3e 100644 --- a/source/lib/assertions/handlers/informational.ts +++ b/source/lib/assertions/handlers/informational.ts @@ -1,6 +1,6 @@ import {CallExpression, TypeChecker, TypeFormatFlags} from '@tsd/typescript'; import {Diagnostic} from '../../interfaces'; -import {makeDiagnostic} from '../../utils'; +import {makeDiagnostic, tsutils} from '../../utils'; /** * Default formatting flags set by TS plus the {@link TypeFormatFlags.NoTruncation NoTruncation} flag. @@ -19,7 +19,7 @@ const typeToStringFormatFlags = * @param nodes - The `printType` AST nodes. * @return List of warning diagnostics containing the type of the first argument. */ -export const prinTypeWarning = (checker: TypeChecker, nodes: Set): Diagnostic[] => { +export const printTypeWarning = (checker: TypeChecker, nodes: Set): Diagnostic[] => { const diagnostics: Diagnostic[] = []; if (!nodes) { @@ -36,3 +36,52 @@ export const prinTypeWarning = (checker: TypeChecker, nodes: Set return diagnostics; }; + +/** + * Asserts that the documentation comment for the argument of the assertion + * includes the string literal generic type of the assertion. + * + * @param checker - The TypeScript type checker. + * @param nodes - The `expectDocCommentIncludes` AST nodes. + * @return List of diagnostics. + */ +export const expectDocCommentIncludes = (checker: TypeChecker, nodes: Set): Diagnostic[] => { + const diagnostics: Diagnostic[] = []; + + if (!nodes) { + return diagnostics; + } + + for (const node of nodes) { + const expression = tsutils.expressionToString(checker, node.arguments[0]) ?? '?'; + + if (!node.typeArguments) { + diagnostics.push(makeDiagnostic(node, `Expected documentation comment for expression \`${expression}\` not specified.`)); + continue; + } + + const maybeExpectedDocComment = checker.getTypeFromTypeNode(node.typeArguments[0]); + + if (!maybeExpectedDocComment.isStringLiteral()) { + diagnostics.push(makeDiagnostic(node, `Expected documentation comment for expression \`${expression}\` should be a string literal.`)); + continue; + } + + const expectedDocComment = maybeExpectedDocComment.value; + const docComment = tsutils.resolveDocComment(checker, node.arguments[0]); + + if (!docComment) { + diagnostics.push(makeDiagnostic(node, `Documentation comment for expression \`${expression}\` not found.`)); + continue; + } + + if (docComment.includes(expectedDocComment)) { + // Do nothing + continue; + } + + diagnostics.push(makeDiagnostic(node, `Documentation comment \`${docComment}\` for expression \`${expression}\` does not include expected \`${expectedDocComment}\`.`)); + } + + return diagnostics; +}; diff --git a/source/lib/assertions/index.ts b/source/lib/assertions/index.ts index a41b8024..6cf6f8e9 100644 --- a/source/lib/assertions/index.ts +++ b/source/lib/assertions/index.ts @@ -8,7 +8,8 @@ import { expectDeprecated, expectNotDeprecated, isNever, - prinTypeWarning, + printTypeWarning, + expectDocCommentIncludes, } from './handlers'; export enum Assertion { @@ -21,6 +22,7 @@ export enum Assertion { EXPECT_NOT_DEPRECATED = 'expectNotDeprecated', EXPECT_NEVER = 'expectNever', PRINT_TYPE = 'printType', + EXPECT_DOC_COMMENT_INCLUDES = 'expectDocCommentIncludes', } // List of diagnostic handlers attached to the assertion @@ -31,7 +33,8 @@ const assertionHandlers = new Map([ [Assertion.EXPECT_DEPRECATED, expectDeprecated], [Assertion.EXPECT_NOT_DEPRECATED, expectNotDeprecated], [Assertion.EXPECT_NEVER, isNever], - [Assertion.PRINT_TYPE, prinTypeWarning] + [Assertion.PRINT_TYPE, printTypeWarning], + [Assertion.EXPECT_DOC_COMMENT_INCLUDES, expectDocCommentIncludes], ]); /** diff --git a/source/lib/utils/typescript.ts b/source/lib/utils/typescript.ts index 9886edb7..7cc126c8 100644 --- a/source/lib/utils/typescript.ts +++ b/source/lib/utils/typescript.ts @@ -1,4 +1,29 @@ -import {TypeChecker, Expression, isCallLikeExpression, JSDocTagInfo} from '@tsd/typescript'; +import {TypeChecker, Expression, isCallLikeExpression, JSDocTagInfo, displayPartsToString} from '@tsd/typescript'; + +const resolveCommentHelper = (resolve: R) => { + type ConditionalResolveReturn = (R extends 'JSDoc' ? Map : string) | undefined; + + const handler = (checker: TypeChecker, expression: Expression): ConditionalResolveReturn => { + const ref = isCallLikeExpression(expression) ? + checker.getResolvedSignature(expression) : + checker.getSymbolAtLocation(expression); + + if (!ref) { + return; + } + + switch (resolve) { + case 'JSDoc': + return new Map(ref.getJsDocTags().map(tag => [tag.name, tag])) as ConditionalResolveReturn; + case 'DocComment': + return displayPartsToString(ref.getDocumentationComment(checker)) as ConditionalResolveReturn; + default: + return undefined; + } + }; + + return handler; +}; /** * 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/ * @param expression - The expression to resolve the JSDoc tags for. * @return A unique Set of JSDoc tags or `undefined` if they couldn't be resolved. */ -export const resolveJSDocTags = (checker: TypeChecker, expression: Expression): Map | undefined => { - const ref = isCallLikeExpression(expression) ? - checker.getResolvedSignature(expression) : - checker.getSymbolAtLocation(expression); +export const resolveJSDocTags = resolveCommentHelper('JSDoc'); - if (!ref) { - return; - } - - return new Map(ref.getJsDocTags().map(tag => [tag.name, tag])); -}; +/** + * Resolve the documentation comment from the expression. If the comment can't be found, it will return `undefined`. + * + * @param checker - The TypeScript type checker. + * @param expression - The expression to resolve the documentation comment for. + * @return A string of the documentation comment or `undefined` if it can't be resolved. + */ +export const resolveDocComment = resolveCommentHelper('DocComment'); /** * Convert a TypeScript expression to a string. diff --git a/source/test/fixtures/informational/expect-doc-comment/index.d.ts b/source/test/fixtures/informational/expect-doc-comment/index.d.ts new file mode 100644 index 00000000..6f3a1c51 --- /dev/null +++ b/source/test/fixtures/informational/expect-doc-comment/index.d.ts @@ -0,0 +1 @@ +export default function (foo: number): number | null; diff --git a/source/test/fixtures/print-type/index.js b/source/test/fixtures/informational/expect-doc-comment/index.js similarity index 100% rename from source/test/fixtures/print-type/index.js rename to source/test/fixtures/informational/expect-doc-comment/index.js diff --git a/source/test/fixtures/informational/expect-doc-comment/index.test-d.ts b/source/test/fixtures/informational/expect-doc-comment/index.test-d.ts new file mode 100644 index 00000000..dced520c --- /dev/null +++ b/source/test/fixtures/informational/expect-doc-comment/index.test-d.ts @@ -0,0 +1,15 @@ +import {expectDocCommentIncludes} from '../../../..'; + +const noDocComment = 'no doc comment'; + +expectDocCommentIncludes<'no doc comment'>(noDocComment); + +/** FooBar */ +const foo = 'bar'; + +expectDocCommentIncludes(foo); +expectDocCommentIncludes(foo); +expectDocCommentIncludes<'BarFoo'>(foo); +expectDocCommentIncludes<'FooBar'>(foo); +expectDocCommentIncludes<'Foo'>(foo); +expectDocCommentIncludes<'Bar'>(foo); diff --git a/source/test/fixtures/print-type/package.json b/source/test/fixtures/informational/expect-doc-comment/package.json similarity index 100% rename from source/test/fixtures/print-type/package.json rename to source/test/fixtures/informational/expect-doc-comment/package.json diff --git a/source/test/fixtures/print-type/index.d.ts b/source/test/fixtures/informational/print-type/index.d.ts similarity index 100% rename from source/test/fixtures/print-type/index.d.ts rename to source/test/fixtures/informational/print-type/index.d.ts diff --git a/source/test/fixtures/informational/print-type/index.js b/source/test/fixtures/informational/print-type/index.js new file mode 100644 index 00000000..0d06f8dd --- /dev/null +++ b/source/test/fixtures/informational/print-type/index.js @@ -0,0 +1,3 @@ +module.exports.default = foo => { + return foo > 0 ? foo : null; +}; diff --git a/source/test/fixtures/print-type/index.test-d.ts b/source/test/fixtures/informational/print-type/index.test-d.ts similarity index 84% rename from source/test/fixtures/print-type/index.test-d.ts rename to source/test/fixtures/informational/print-type/index.test-d.ts index 55ac5119..efe2fd54 100644 --- a/source/test/fixtures/print-type/index.test-d.ts +++ b/source/test/fixtures/informational/print-type/index.test-d.ts @@ -1,4 +1,4 @@ -import {printType} from '../../..'; +import {printType} from '../../../..'; import {aboveZero, bigType} from '.'; printType(aboveZero); diff --git a/source/test/fixtures/informational/print-type/package.json b/source/test/fixtures/informational/print-type/package.json new file mode 100644 index 00000000..de6dc1db --- /dev/null +++ b/source/test/fixtures/informational/print-type/package.json @@ -0,0 +1,3 @@ +{ + "name": "foo" +} diff --git a/source/test/informational.ts b/source/test/informational.ts new file mode 100644 index 00000000..1f364d4e --- /dev/null +++ b/source/test/informational.ts @@ -0,0 +1,30 @@ +import path from 'path'; +import test from 'ava'; +import {verify} from './fixtures/utils'; +import tsd from '..'; + +test('print type', async t => { + const diagnostics = await tsd({cwd: path.join(__dirname, 'fixtures/informational/print-type')}); + + verify(t, diagnostics, [ + [4, 0, 'warning', 'Type for expression `aboveZero` is: `(foo: number) => number | null`'], + [5, 0, 'warning', 'Type for expression `null` is: `null`'], + [6, 0, 'warning', 'Type for expression `undefined` is: `undefined`'], + [7, 0, 'warning', 'Type for expression `null as any` is: `any`'], + [8, 0, 'warning', 'Type for expression `null as never` is: `never`'], + [9, 0, 'warning', 'Type for expression `null as unknown` is: `unknown`'], + [10, 0, 'warning', 'Type for expression `\'foo\'` is: `"foo"`'], + [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; }`'], + ]); +}); + +test('expect doc comment includes', async t => { + const diagnostics = await tsd({cwd: path.join(__dirname, 'fixtures/informational/expect-doc-comment')}); + + verify(t, diagnostics, [ + [5, 0, 'error', 'Documentation comment for expression `noDocComment` not found.'], + [10, 0, 'error', 'Expected documentation comment for expression `foo` not specified.'], + [11, 0, 'error', 'Expected documentation comment for expression `foo` should be a string literal.'], + [12, 0, 'error', 'Documentation comment `FooBar` for expression `foo` does not include expected `BarFoo`.'], + ]); +}); diff --git a/source/test/test.ts b/source/test/test.ts index 3529dd0f..4c44b1e7 100644 --- a/source/test/test.ts +++ b/source/test/test.ts @@ -442,21 +442,6 @@ test('allow specifying `rootDir` option in `tsconfig.json`', async t => { verify(t, diagnostics, []); }); -test('prints the types of expressions passed to `printType` helper', async t => { - const diagnostics = await tsd({cwd: path.join(__dirname, 'fixtures/print-type')}); - - verify(t, diagnostics, [ - [4, 0, 'warning', 'Type for expression `aboveZero` is: `(foo: number) => number | null`'], - [5, 0, 'warning', 'Type for expression `null` is: `null`'], - [6, 0, 'warning', 'Type for expression `undefined` is: `undefined`'], - [7, 0, 'warning', 'Type for expression `null as any` is: `any`'], - [8, 0, 'warning', 'Type for expression `null as never` is: `never`'], - [9, 0, 'warning', 'Type for expression `null as unknown` is: `unknown`'], - [10, 0, 'warning', 'Type for expression `\'foo\'` is: `"foo"`'], - [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; }`'] - ]); -}); - test('assertions should be identified if imported as an aliased module', async t => { const diagnostics = await tsd({cwd: path.join(__dirname, 'fixtures/aliased/aliased-module')});