Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement printType helper to print the type of the passed expression as a warning #115

Merged
merged 3 commits into from Jun 3, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 @@ -390,3 +390,17 @@ test('errors in libs from node_modules are not reported', async t => {
[3, 18, 'error', 'Cannot find name \'Bar\'.']
]);
});

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"`'],
]);
});