Skip to content

Commit

Permalink
Implement printType helper to print the type of the passed expressi…
Browse files Browse the repository at this point in the history
…on as a warning (#115)
  • Loading branch information
BendingBender committed Jun 3, 2021
1 parent b749113 commit 6480ca9
Show file tree
Hide file tree
Showing 13 changed files with 95 additions and 11 deletions.
6 changes: 6 additions & 0 deletions readme.md
Expand Up @@ -189,6 +189,12 @@ Check that `value` is marked a [`@deprecated`](https://jsdoc.app/tags-deprecated

Check that `value` is not marked a [`@deprecated`](https://jsdoc.app/tags-deprecated.html).

### printType(value)

Print the type of `value` as a warning.

Useful if you don't know the exact type of the expression passed to `printType()` or the type is too complex to write out by hand.


## Programmatic API

Expand Down
10 changes: 10 additions & 0 deletions source/lib/assertions/assert.ts
Expand Up @@ -68,3 +68,13 @@ export const expectDeprecated = (expression: any) => { // tslint:disable-line:n
export const expectNotDeprecated = (expression: any) => { // tslint:disable-line:no-unused
// Do nothing, the TypeScript compiler handles this for us
};

/**
* Will print a warning with the type of the expression passed as argument.
*
* @param expression - Expression whose type should be printed as a warning.
*/
// @ts-ignore
export const printType = (expression: any) => { // tslint:disable-line:no-unused
// Do nothing, the TypeScript compiler handles this for us
};
2 changes: 1 addition & 1 deletion source/lib/assertions/handlers/identicality.ts
Expand Up @@ -55,7 +55,7 @@ export const isIdentical = (checker: TypeChecker, nodes: Set<CallExpression>): D
* 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.
* @param nodes - The `expectNotType` AST nodes.
* @return List of custom diagnostics.
*/
export const isNotIdentical = (checker: TypeChecker, nodes: Set<CallExpression>): Diagnostic[] => {
Expand Down
1 change: 1 addition & 0 deletions source/lib/assertions/handlers/index.ts
Expand Up @@ -4,3 +4,4 @@ export {Handler} from './handler';
export {isIdentical, isNotIdentical} from './identicality';
export {isNotAssignable} from './assignability';
export {expectDeprecated, expectNotDeprecated} from './expect-deprecated';
export {prinTypeWarning} from './informational';
27 changes: 27 additions & 0 deletions source/lib/assertions/handlers/informational.ts
@@ -0,0 +1,27 @@
import {CallExpression, TypeChecker} from '@tsd/typescript';
import {Diagnostic} from '../../interfaces';
import {makeDiagnostic} from '../../utils';

/**
* Emits a warning diagnostic for every call experession encountered containing the type of the first argument.
*
* @param checker - The TypeScript type checker.
* @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<CallExpression>): Diagnostic[] => {
const diagnostics: Diagnostic[] = [];

if (!nodes) {
return diagnostics;
}

for (const node of nodes) {
const argumentType = checker.getTypeAtLocation(node.arguments[0]);
const argumentExpression = node.arguments[0].getText();

diagnostics.push(makeDiagnostic(node, `Type for expression \`${argumentExpression}\` is: \`${checker.typeToString(argumentType)}\``, 'warning'));
}

return diagnostics;
};
16 changes: 13 additions & 3 deletions source/lib/assertions/index.ts
@@ -1,6 +1,14 @@
import {CallExpression, TypeChecker} from '@tsd/typescript';
import {Diagnostic} from '../interfaces';
import {Handler, isIdentical, isNotIdentical, isNotAssignable, expectDeprecated, expectNotDeprecated} from './handlers';
import {
Handler,
isIdentical,
isNotIdentical,
isNotAssignable,
expectDeprecated,
expectNotDeprecated,
prinTypeWarning,
} from './handlers';

export enum Assertion {
EXPECT_TYPE = 'expectType',
Expand All @@ -9,7 +17,8 @@ export enum Assertion {
EXPECT_ASSIGNABLE = 'expectAssignable',
EXPECT_NOT_ASSIGNABLE = 'expectNotAssignable',
EXPECT_DEPRECATED = 'expectDeprecated',
EXPECT_NOT_DEPRECATED = 'expectNotDeprecated'
EXPECT_NOT_DEPRECATED = 'expectNotDeprecated',
PRINT_TYPE = 'printType',
}

// List of diagnostic handlers attached to the assertion
Expand All @@ -18,7 +27,8 @@ const assertionHandlers = new Map<Assertion, Handler>([
[Assertion.EXPECT_NOT_TYPE, isNotIdentical],
[Assertion.EXPECT_NOT_ASSIGNABLE, isNotAssignable],
[Assertion.EXPECT_DEPRECATED, expectDeprecated],
[Assertion.EXPECT_NOT_DEPRECATED, expectNotDeprecated]
[Assertion.EXPECT_NOT_DEPRECATED, expectNotDeprecated],
[Assertion.PRINT_TYPE, prinTypeWarning]
]);

/**
Expand Down
9 changes: 4 additions & 5 deletions source/lib/parser.ts
Expand Up @@ -2,8 +2,7 @@ import {Program, Node, CallExpression, forEachChild, isCallExpression, Identifie
import {Assertion} from './assertions';
import {Location, Diagnostic} from './interfaces';

// TODO: Use Object.values() when targetting Node.js >= 8
const assertionTypes = new Set<string>(Object.keys(Assertion).map(key => Assertion[key]));
const assertionFnNames = new Set<string>(Object.values(Assertion));

/**
* Extract all assertions.
Expand All @@ -18,11 +17,11 @@ export const extractAssertions = (program: Program): Map<Assertion, Set<CallExpr
*/
function walkNodes(node: Node) {
if (isCallExpression(node)) {
const text = (node.expression as Identifier).getText();
const identifier = (node.expression as Identifier).getText();

// Check if the call type is a valid assertion
if (assertionTypes.has(text)) {
const assertion = text as Assertion;
if (assertionFnNames.has(identifier)) {
const assertion = identifier as Assertion;

const nodes = assertions.get(assertion) || new Set<CallExpression>();

Expand Down
1 change: 1 addition & 0 deletions source/test/fixtures/print-type/index.d.ts
@@ -0,0 +1 @@
export default function (foo: number): number | null;
3 changes: 3 additions & 0 deletions source/test/fixtures/print-type/index.js
@@ -0,0 +1,3 @@
module.exports.default = foo => {
return foo > 0 ? foo : null;
};
10 changes: 10 additions & 0 deletions source/test/fixtures/print-type/index.test-d.ts
@@ -0,0 +1,10 @@
import {printType} from '../../..';
import aboveZero from '.';

printType(aboveZero);
printType(null);
printType(undefined);
printType(null as any);
printType(null as never);
printType(null as unknown);
printType('foo');
3 changes: 3 additions & 0 deletions source/test/fixtures/print-type/package.json
@@ -0,0 +1,3 @@
{
"name": "foo"
}
4 changes: 2 additions & 2 deletions source/test/fixtures/utils.ts
Expand Up @@ -9,7 +9,7 @@ type Expectation = [
message: string,
];

type ExpectationWithFilename = [
type ExpectationWithFileName = [
line: number,
column: number,
severity: 'error' | 'warning',
Expand Down Expand Up @@ -54,7 +54,7 @@ export const verifyWithFileName = (
t: ExecutionContext,
cwd: string,
diagnostics: Diagnostic[],
expectations: ExpectationWithFilename[]
expectations: ExpectationWithFileName[]
) => {
const diagnosticObjs = diagnostics.map(({line, column, severity, message, fileName}) => ({
line,
Expand Down
14 changes: 14 additions & 0 deletions source/test/test.ts
Expand Up @@ -416,3 +416,17 @@ 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"`'],
]);
});

0 comments on commit 6480ca9

Please sign in to comment.