Skip to content

Commit

Permalink
Add deprecation expectations - fixes #51 (#53)
Browse files Browse the repository at this point in the history
  • Loading branch information
SamVerschueren committed Nov 13, 2019
1 parent 3a375fa commit 97261b2
Show file tree
Hide file tree
Showing 16 changed files with 307 additions and 5 deletions.
8 changes: 8 additions & 0 deletions readme.md
Expand Up @@ -170,6 +170,14 @@ Check if the function call has argument type errors.

Check if a value is of the provided type `T`.

### expectDeprecated(value)

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

### expectNotDeprecated(value)

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


## Programmatic API

Expand Down
20 changes: 20 additions & 0 deletions source/lib/assertions/assert.ts
Expand Up @@ -37,3 +37,23 @@ export const expectNotAssignable = <T>(value: any) => { // tslint:disable-line:
export const expectError = <T = any>(value: T) => { // tslint:disable-line:no-unused
// Do nothing, the TypeScript compiler handles this for us
};

/**
* Assert that the `expression` provided is marked as `@deprecated`.
*
* @param expression - Expression that should be marked as `@deprecated`.
*/
// @ts-ignore
export const expectDeprecated = (expression: any) => { // tslint:disable-line:no-unused
// Do nothing, the TypeScript compiler handles this for us
};

/**
* Assert that the `expression` provided is not marked as `@deprecated`.
*
* @param expression - Expression that should not be marked as `@deprecated`.
*/
// @ts-ignore
export const expectNotDeprecated = (expression: any) => { // tslint:disable-line:no-unused
// Do nothing, the TypeScript compiler handles this for us
};
63 changes: 63 additions & 0 deletions source/lib/assertions/handlers/expect-deprecated.ts
@@ -0,0 +1,63 @@
import {JSDocTagInfo} from '../../../../libraries/typescript/lib/typescript';
import {Diagnostic} from '../../interfaces';
import {Handler} from './handler';
import {makeDiagnostic, tsutils} from '../../utils';

interface Options {
filter(tags: Map<string, JSDocTagInfo>): boolean;
message(signature: string): string;
}

const expectDeprecatedHelper = (options: Options): Handler => {
return (checker, nodes) => {
const diagnostics: Diagnostic[] = [];

if (!nodes) {
// Bail out if we don't have any nodes
return diagnostics;
}

for (const node of nodes) {
const argument = node.arguments[0];

const tags = tsutils.resolveJSDocTags(checker, argument);

if (!tags || !options.filter(tags)) {
// Bail out if not tags couldn't be resolved or when the node matches the filter expression
continue;
}

const message = tsutils.expressionToString(checker, argument);

diagnostics.push(makeDiagnostic(node, options.message(message || '?')));
}

return diagnostics;
};
};

/**
* Assert that the argument from the `expectDeprecated` statement is marked as `@deprecated`.
* If it's not marked as `@deprecated`, an error diagnostic is returned.
*
* @param checker - The TypeScript type checker.
* @param nodes - The `expectDeprecated` AST nodes.
* @return List of diagnostics.
*/
export const expectDeprecated = expectDeprecatedHelper({
filter: tags => !tags.has('deprecated'),
message: signature => `Expected \`${signature}\` to be marked as \`@deprecated\``
});

/**
* Assert that the argument from the `expectNotDeprecated` statement is not marked as `@deprecated`.
* If it's marked as `@deprecated`, an error diagnostic is returned.
*
* @param checker - The TypeScript type checker.
* @param nodes - The `expectNotDeprecated` AST nodes.
* @return List of diagnostics.
*/
export const expectNotDeprecated = expectDeprecatedHelper({
filter: tags => tags.has('deprecated'),
message: signature => `Expected \`${signature}\` to not be marked as \`@deprecated\``
});
1 change: 1 addition & 0 deletions source/lib/assertions/handlers/index.ts
Expand Up @@ -3,3 +3,4 @@ export {Handler} from './handler';
// Handlers
export {strictAssertion} from './strict-assertion';
export {isNotAssignable} from './assignability';
export {expectDeprecated, expectNotDeprecated} from './expect-deprecated';
11 changes: 7 additions & 4 deletions source/lib/assertions/index.ts
@@ -1,20 +1,23 @@
import {CallExpression} from '../../../libraries/typescript/lib/typescript';
import {TypeChecker} from '../entities/typescript';
import {Diagnostic} from '../interfaces';
import {Handler, strictAssertion} from './handlers';
import {isNotAssignable} from './handlers/assignability';
import {Handler, strictAssertion, isNotAssignable, expectDeprecated, expectNotDeprecated} from './handlers';

export enum Assertion {
EXPECT_TYPE = 'expectType',
EXPECT_ERROR = 'expectError',
EXPECT_ASSIGNABLE = 'expectAssignable',
EXPECT_NOT_ASSIGNABLE = 'expectNotAssignable'
EXPECT_NOT_ASSIGNABLE = 'expectNotAssignable',
EXPECT_DEPRECATED = 'expectDeprecated',
EXPECT_NOT_DEPRECATED = 'expectNotDeprecated'
}

// List of diagnostic handlers attached to the assertion
const assertionHandlers = new Map<string, Handler | Handler[]>([
[Assertion.EXPECT_TYPE, strictAssertion],
[Assertion.EXPECT_NOT_ASSIGNABLE, isNotAssignable]
[Assertion.EXPECT_NOT_ASSIGNABLE, isNotAssignable],
[Assertion.EXPECT_DEPRECATED, expectDeprecated],
[Assertion.EXPECT_NOT_DEPRECATED, expectNotDeprecated]
]);

/**
Expand Down
4 changes: 3 additions & 1 deletion source/lib/utils/index.ts
@@ -1,7 +1,9 @@
import makeDiagnostic from './make-diagnostic';
import getJSONPropertyPosition from './get-json-property-position';
import * as tsutils from './typescript';

export {
getJSONPropertyPosition,
makeDiagnostic
makeDiagnostic,
tsutils
};
47 changes: 47 additions & 0 deletions source/lib/utils/typescript.ts
@@ -0,0 +1,47 @@
import {TypeChecker, Expression, isCallLikeExpression, JSDocTagInfo} from '../../../libraries/typescript/lib/typescript';

/**
* Resolve the JSDoc tags from the expression. If these tags couldn't be found, it will return `undefined`.
*
* @param checker - The TypeScript type checker.
* @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<string, JSDocTagInfo> | undefined => {
const ref = isCallLikeExpression(expression)
? checker.getResolvedSignature(expression)
: checker.getSymbolAtLocation(expression);

if (!ref) {
return;
}

return new Map<string, JSDocTagInfo>(ref.getJsDocTags().map(tag => [tag.name, tag]));
};

/**
* Convert a TypeScript expression to a string.
*
* @param checker - The TypeScript type checker.
* @param expression - The expression to convert.
* @return The string representation of the expression or `undefined` if it couldn't be resolved.
*/
export const expressionToString = (checker: TypeChecker, expression: Expression): string | undefined => {
if (isCallLikeExpression(expression)) {
const signature = checker.getResolvedSignature(expression);

if (!signature) {
return;
}

return checker.signatureToString(signature);
}

const symbol = checker.getSymbolAtLocation(expression);

if (!symbol) {
return;
}

return checker.symbolToString(symbol, expression);
};
26 changes: 26 additions & 0 deletions source/test/deprecated.ts
@@ -0,0 +1,26 @@
import * as path from 'path';
import test from 'ava';
import {verify} from './fixtures/utils';
import tsd from '..';

test('deprecated', async t => {
const diagnostics = await tsd({cwd: path.join(__dirname, 'fixtures/deprecated/expect-deprecated')});

verify(t, diagnostics, [
[6, 0, 'error', 'Expected `(foo: number, bar: number): number` to be marked as `@deprecated`'],
[15, 0, 'error', 'Expected `Options.delimiter` to be marked as `@deprecated`'],
[19, 0, 'error', 'Expected `Unicorn.RAINBOW` to be marked as `@deprecated`'],
[34, 0, 'error', 'Expected `RainbowClass` to be marked as `@deprecated`']
]);
});

test('not deprecated', async t => {
const diagnostics = await tsd({cwd: path.join(__dirname, 'fixtures/deprecated/expect-not-deprecated')});

verify(t, diagnostics, [
[5, 0, 'error', 'Expected `(foo: string, bar: string): string` to not be marked as `@deprecated`'],
[14, 0, 'error', 'Expected `Options.separator` to not be marked as `@deprecated`'],
[18, 0, 'error', 'Expected `Unicorn.UNICORN` to not be marked as `@deprecated`'],
[33, 0, 'error', 'Expected `UnicornClass` to not be marked as `@deprecated`']
]);
});
26 changes: 26 additions & 0 deletions source/test/fixtures/deprecated/expect-deprecated/index.d.ts
@@ -0,0 +1,26 @@
export interface Options {
/**
* @deprecated
*/
readonly separator: string;
readonly delimiter: string;
}

declare const concat: {
/**
* @deprecated
*/
(foo: string, bar: string): string;
(foo: string, bar: string, options: Options): string;
(foo: number, bar: number): number;
};

export const enum Unicorn {
/**
* @deprecated
*/
UNICORN = '🦄',
RAINBOW = '🌈'
}

export default concat;
3 changes: 3 additions & 0 deletions source/test/fixtures/deprecated/expect-deprecated/index.js
@@ -0,0 +1,3 @@
module.exports.default = (foo, bar) => {
return foo + bar;
};
34 changes: 34 additions & 0 deletions source/test/fixtures/deprecated/expect-deprecated/index.test-d.ts
@@ -0,0 +1,34 @@
import {expectDeprecated} from '../../../..';
import concat, {Unicorn, Options} from '.';

// Methods
expectDeprecated(concat('foo', 'bar'));
expectDeprecated(concat(1, 2));

// Properties
const options: Options = {
separator: ',',
delimiter: '/'
};

expectDeprecated(options.separator);
expectDeprecated(options.delimiter);

// ENUM
expectDeprecated(Unicorn.UNICORN);
expectDeprecated(Unicorn.RAINBOW);

// Classes
/**
* @deprecated
*/
class UnicornClass {
readonly key = '🦄';
}

class RainbowClass {
readonly key = '🌈';
}

expectDeprecated(UnicornClass);
expectDeprecated(RainbowClass);
@@ -0,0 +1,3 @@
{
"name": "foo"
}
26 changes: 26 additions & 0 deletions source/test/fixtures/deprecated/expect-not-deprecated/index.d.ts
@@ -0,0 +1,26 @@
export interface Options {
/**
* @deprecated
*/
readonly separator: string;
readonly delimiter: string;
}

declare const concat: {
/**
* @deprecated
*/
(foo: string, bar: string): string;
(foo: string, bar: string, options: Options): string;
(foo: number, bar: number): number;
};

export const enum Unicorn {
/**
* @deprecated
*/
UNICORN = '🦄',
RAINBOW = '🌈'
}

export default concat;
@@ -0,0 +1,3 @@
module.exports.default = (foo, bar) => {
return foo + bar;
};
@@ -0,0 +1,34 @@
import {expectNotDeprecated} from '../../../..';
import concat, {Unicorn, Options} from '.';

// Methods
expectNotDeprecated(concat('foo', 'bar'));
expectNotDeprecated(concat(1, 2));

// Properties
const options: Options = {
separator: ',',
delimiter: '/'
};

expectNotDeprecated(options.separator);
expectNotDeprecated(options.delimiter);

// ENUM
expectNotDeprecated(Unicorn.UNICORN);
expectNotDeprecated(Unicorn.RAINBOW);

// Classes
/**
* @deprecated
*/
class UnicornClass {
readonly key = '🦄';
}

class RainbowClass {
readonly key = '🌈';
}

expectNotDeprecated(UnicornClass);
expectNotDeprecated(RainbowClass);
@@ -0,0 +1,3 @@
{
"name": "foo"
}

0 comments on commit 97261b2

Please sign in to comment.