Skip to content

Commit 97261b2

Browse files
Add deprecation expectations - fixes #51 (#53)
1 parent 3a375fa commit 97261b2

File tree

16 files changed

+307
-5
lines changed

16 files changed

+307
-5
lines changed

readme.md

+8
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,14 @@ Check if the function call has argument type errors.
170170

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

173+
### expectDeprecated(value)
174+
175+
Check that `value` is marked a [`@deprecated`](https://jsdoc.app/tags-deprecated.html).
176+
177+
### expectNotDeprecated(value)
178+
179+
Check that `value` is not marked a [`@deprecated`](https://jsdoc.app/tags-deprecated.html).
180+
173181

174182
## Programmatic API
175183

source/lib/assertions/assert.ts

+20
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,23 @@ export const expectNotAssignable = <T>(value: any) => { // tslint:disable-line:
3737
export const expectError = <T = any>(value: T) => { // tslint:disable-line:no-unused
3838
// Do nothing, the TypeScript compiler handles this for us
3939
};
40+
41+
/**
42+
* Assert that the `expression` provided is marked as `@deprecated`.
43+
*
44+
* @param expression - Expression that should be marked as `@deprecated`.
45+
*/
46+
// @ts-ignore
47+
export const expectDeprecated = (expression: any) => { // tslint:disable-line:no-unused
48+
// Do nothing, the TypeScript compiler handles this for us
49+
};
50+
51+
/**
52+
* Assert that the `expression` provided is not marked as `@deprecated`.
53+
*
54+
* @param expression - Expression that should not be marked as `@deprecated`.
55+
*/
56+
// @ts-ignore
57+
export const expectNotDeprecated = (expression: any) => { // tslint:disable-line:no-unused
58+
// Do nothing, the TypeScript compiler handles this for us
59+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import {JSDocTagInfo} from '../../../../libraries/typescript/lib/typescript';
2+
import {Diagnostic} from '../../interfaces';
3+
import {Handler} from './handler';
4+
import {makeDiagnostic, tsutils} from '../../utils';
5+
6+
interface Options {
7+
filter(tags: Map<string, JSDocTagInfo>): boolean;
8+
message(signature: string): string;
9+
}
10+
11+
const expectDeprecatedHelper = (options: Options): Handler => {
12+
return (checker, nodes) => {
13+
const diagnostics: Diagnostic[] = [];
14+
15+
if (!nodes) {
16+
// Bail out if we don't have any nodes
17+
return diagnostics;
18+
}
19+
20+
for (const node of nodes) {
21+
const argument = node.arguments[0];
22+
23+
const tags = tsutils.resolveJSDocTags(checker, argument);
24+
25+
if (!tags || !options.filter(tags)) {
26+
// Bail out if not tags couldn't be resolved or when the node matches the filter expression
27+
continue;
28+
}
29+
30+
const message = tsutils.expressionToString(checker, argument);
31+
32+
diagnostics.push(makeDiagnostic(node, options.message(message || '?')));
33+
}
34+
35+
return diagnostics;
36+
};
37+
};
38+
39+
/**
40+
* Assert that the argument from the `expectDeprecated` statement is marked as `@deprecated`.
41+
* If it's not marked as `@deprecated`, an error diagnostic is returned.
42+
*
43+
* @param checker - The TypeScript type checker.
44+
* @param nodes - The `expectDeprecated` AST nodes.
45+
* @return List of diagnostics.
46+
*/
47+
export const expectDeprecated = expectDeprecatedHelper({
48+
filter: tags => !tags.has('deprecated'),
49+
message: signature => `Expected \`${signature}\` to be marked as \`@deprecated\``
50+
});
51+
52+
/**
53+
* Assert that the argument from the `expectNotDeprecated` statement is not marked as `@deprecated`.
54+
* If it's marked as `@deprecated`, an error diagnostic is returned.
55+
*
56+
* @param checker - The TypeScript type checker.
57+
* @param nodes - The `expectNotDeprecated` AST nodes.
58+
* @return List of diagnostics.
59+
*/
60+
export const expectNotDeprecated = expectDeprecatedHelper({
61+
filter: tags => tags.has('deprecated'),
62+
message: signature => `Expected \`${signature}\` to not be marked as \`@deprecated\``
63+
});

source/lib/assertions/handlers/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ export {Handler} from './handler';
33
// Handlers
44
export {strictAssertion} from './strict-assertion';
55
export {isNotAssignable} from './assignability';
6+
export {expectDeprecated, expectNotDeprecated} from './expect-deprecated';

source/lib/assertions/index.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,23 @@
11
import {CallExpression} from '../../../libraries/typescript/lib/typescript';
22
import {TypeChecker} from '../entities/typescript';
33
import {Diagnostic} from '../interfaces';
4-
import {Handler, strictAssertion} from './handlers';
5-
import {isNotAssignable} from './handlers/assignability';
4+
import {Handler, strictAssertion, isNotAssignable, expectDeprecated, expectNotDeprecated} from './handlers';
65

76
export enum Assertion {
87
EXPECT_TYPE = 'expectType',
98
EXPECT_ERROR = 'expectError',
109
EXPECT_ASSIGNABLE = 'expectAssignable',
11-
EXPECT_NOT_ASSIGNABLE = 'expectNotAssignable'
10+
EXPECT_NOT_ASSIGNABLE = 'expectNotAssignable',
11+
EXPECT_DEPRECATED = 'expectDeprecated',
12+
EXPECT_NOT_DEPRECATED = 'expectNotDeprecated'
1213
}
1314

1415
// List of diagnostic handlers attached to the assertion
1516
const assertionHandlers = new Map<string, Handler | Handler[]>([
1617
[Assertion.EXPECT_TYPE, strictAssertion],
17-
[Assertion.EXPECT_NOT_ASSIGNABLE, isNotAssignable]
18+
[Assertion.EXPECT_NOT_ASSIGNABLE, isNotAssignable],
19+
[Assertion.EXPECT_DEPRECATED, expectDeprecated],
20+
[Assertion.EXPECT_NOT_DEPRECATED, expectNotDeprecated]
1821
]);
1922

2023
/**

source/lib/utils/index.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import makeDiagnostic from './make-diagnostic';
22
import getJSONPropertyPosition from './get-json-property-position';
3+
import * as tsutils from './typescript';
34

45
export {
56
getJSONPropertyPosition,
6-
makeDiagnostic
7+
makeDiagnostic,
8+
tsutils
79
};

source/lib/utils/typescript.ts

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import {TypeChecker, Expression, isCallLikeExpression, JSDocTagInfo} from '../../../libraries/typescript/lib/typescript';
2+
3+
/**
4+
* Resolve the JSDoc tags from the expression. If these tags couldn't be found, it will return `undefined`.
5+
*
6+
* @param checker - The TypeScript type checker.
7+
* @param expression - The expression to resolve the JSDoc tags for.
8+
* @return A unique Set of JSDoc tags or `undefined` if they couldn't be resolved.
9+
*/
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);
14+
15+
if (!ref) {
16+
return;
17+
}
18+
19+
return new Map<string, JSDocTagInfo>(ref.getJsDocTags().map(tag => [tag.name, tag]));
20+
};
21+
22+
/**
23+
* Convert a TypeScript expression to a string.
24+
*
25+
* @param checker - The TypeScript type checker.
26+
* @param expression - The expression to convert.
27+
* @return The string representation of the expression or `undefined` if it couldn't be resolved.
28+
*/
29+
export const expressionToString = (checker: TypeChecker, expression: Expression): string | undefined => {
30+
if (isCallLikeExpression(expression)) {
31+
const signature = checker.getResolvedSignature(expression);
32+
33+
if (!signature) {
34+
return;
35+
}
36+
37+
return checker.signatureToString(signature);
38+
}
39+
40+
const symbol = checker.getSymbolAtLocation(expression);
41+
42+
if (!symbol) {
43+
return;
44+
}
45+
46+
return checker.symbolToString(symbol, expression);
47+
};

source/test/deprecated.ts

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import * as path from 'path';
2+
import test from 'ava';
3+
import {verify} from './fixtures/utils';
4+
import tsd from '..';
5+
6+
test('deprecated', async t => {
7+
const diagnostics = await tsd({cwd: path.join(__dirname, 'fixtures/deprecated/expect-deprecated')});
8+
9+
verify(t, diagnostics, [
10+
[6, 0, 'error', 'Expected `(foo: number, bar: number): number` to be marked as `@deprecated`'],
11+
[15, 0, 'error', 'Expected `Options.delimiter` to be marked as `@deprecated`'],
12+
[19, 0, 'error', 'Expected `Unicorn.RAINBOW` to be marked as `@deprecated`'],
13+
[34, 0, 'error', 'Expected `RainbowClass` to be marked as `@deprecated`']
14+
]);
15+
});
16+
17+
test('not deprecated', async t => {
18+
const diagnostics = await tsd({cwd: path.join(__dirname, 'fixtures/deprecated/expect-not-deprecated')});
19+
20+
verify(t, diagnostics, [
21+
[5, 0, 'error', 'Expected `(foo: string, bar: string): string` to not be marked as `@deprecated`'],
22+
[14, 0, 'error', 'Expected `Options.separator` to not be marked as `@deprecated`'],
23+
[18, 0, 'error', 'Expected `Unicorn.UNICORN` to not be marked as `@deprecated`'],
24+
[33, 0, 'error', 'Expected `UnicornClass` to not be marked as `@deprecated`']
25+
]);
26+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
export interface Options {
2+
/**
3+
* @deprecated
4+
*/
5+
readonly separator: string;
6+
readonly delimiter: string;
7+
}
8+
9+
declare const concat: {
10+
/**
11+
* @deprecated
12+
*/
13+
(foo: string, bar: string): string;
14+
(foo: string, bar: string, options: Options): string;
15+
(foo: number, bar: number): number;
16+
};
17+
18+
export const enum Unicorn {
19+
/**
20+
* @deprecated
21+
*/
22+
UNICORN = '🦄',
23+
RAINBOW = '🌈'
24+
}
25+
26+
export default concat;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module.exports.default = (foo, bar) => {
2+
return foo + bar;
3+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import {expectDeprecated} from '../../../..';
2+
import concat, {Unicorn, Options} from '.';
3+
4+
// Methods
5+
expectDeprecated(concat('foo', 'bar'));
6+
expectDeprecated(concat(1, 2));
7+
8+
// Properties
9+
const options: Options = {
10+
separator: ',',
11+
delimiter: '/'
12+
};
13+
14+
expectDeprecated(options.separator);
15+
expectDeprecated(options.delimiter);
16+
17+
// ENUM
18+
expectDeprecated(Unicorn.UNICORN);
19+
expectDeprecated(Unicorn.RAINBOW);
20+
21+
// Classes
22+
/**
23+
* @deprecated
24+
*/
25+
class UnicornClass {
26+
readonly key = '🦄';
27+
}
28+
29+
class RainbowClass {
30+
readonly key = '🌈';
31+
}
32+
33+
expectDeprecated(UnicornClass);
34+
expectDeprecated(RainbowClass);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"name": "foo"
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
export interface Options {
2+
/**
3+
* @deprecated
4+
*/
5+
readonly separator: string;
6+
readonly delimiter: string;
7+
}
8+
9+
declare const concat: {
10+
/**
11+
* @deprecated
12+
*/
13+
(foo: string, bar: string): string;
14+
(foo: string, bar: string, options: Options): string;
15+
(foo: number, bar: number): number;
16+
};
17+
18+
export const enum Unicorn {
19+
/**
20+
* @deprecated
21+
*/
22+
UNICORN = '🦄',
23+
RAINBOW = '🌈'
24+
}
25+
26+
export default concat;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module.exports.default = (foo, bar) => {
2+
return foo + bar;
3+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import {expectNotDeprecated} from '../../../..';
2+
import concat, {Unicorn, Options} from '.';
3+
4+
// Methods
5+
expectNotDeprecated(concat('foo', 'bar'));
6+
expectNotDeprecated(concat(1, 2));
7+
8+
// Properties
9+
const options: Options = {
10+
separator: ',',
11+
delimiter: '/'
12+
};
13+
14+
expectNotDeprecated(options.separator);
15+
expectNotDeprecated(options.delimiter);
16+
17+
// ENUM
18+
expectNotDeprecated(Unicorn.UNICORN);
19+
expectNotDeprecated(Unicorn.RAINBOW);
20+
21+
// Classes
22+
/**
23+
* @deprecated
24+
*/
25+
class UnicornClass {
26+
readonly key = '🦄';
27+
}
28+
29+
class RainbowClass {
30+
readonly key = '🌈';
31+
}
32+
33+
expectNotDeprecated(UnicornClass);
34+
expectNotDeprecated(RainbowClass);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"name": "foo"
3+
}

0 commit comments

Comments
 (0)