Skip to content

Commit 5d07db2

Browse files
authored
feat: extract generic types and nullable information as separate fields (#88)
1 parent bf323dc commit 5d07db2

File tree

10 files changed

+110
-16
lines changed

10 files changed

+110
-16
lines changed

fixtures/test-utils/advanced-types/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ export class TestUtilWrapper {
88
return [];
99
}
1010

11+
findSomething(): TestUtilWrapper | null {
12+
return Math.random() > 0.5 ? new TestUtilWrapper() : null;
13+
}
14+
1115
/**
1216
* Generic arguments
1317
*/
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { ElementWrapper } from './core';
5+
6+
class CardWrapper extends ElementWrapper {
7+
findHeader() {
8+
return new ElementWrapper();
9+
}
10+
11+
findContent() {
12+
return new ElementWrapper();
13+
}
14+
}
15+
16+
export class CardsWrapper extends ElementWrapper {
17+
findItems() {
18+
return [new CardWrapper(), new CardWrapper()];
19+
}
20+
}

fixtures/test-utils/exports/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
// SPDX-License-Identifier: Apache-2.0
33
export { AlertWrapper } from './alert';
44
export { ButtonWrapper } from './button';
5+
export { CardsWrapper } from './cards';

src/components/type-utils.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@ export function isOptional(type: ts.Type) {
1010
return !!type.types.find(t => t.flags & ts.TypeFlags.Undefined);
1111
}
1212

13+
export function isNullable(type: ts.Type) {
14+
if (!type.isUnionOrIntersection()) {
15+
return false;
16+
}
17+
return !!type.types.find(t => t.flags & ts.TypeFlags.Null);
18+
}
19+
1320
export function unwrapNamespaceDeclaration(declaration: ts.Declaration | undefined) {
1421
if (!declaration) {
1522
return [];
@@ -114,3 +121,16 @@ export function printFlags(flags: number, mapping: Record<number, string>) {
114121
.map(([key, value]) => value)
115122
.join(' | ');
116123
}
124+
125+
export function extractTypeArguments(type: ts.Type, checker: ts.TypeChecker) {
126+
const typeParameters = checker.getTypeArguments(type as ts.TypeReference);
127+
128+
if (typeParameters.length <= 0) {
129+
return { typeName: stringifyType(type, checker) };
130+
}
131+
const symbol = type.getSymbol();
132+
if (!symbol) {
133+
throw new Error(`Unknown generic type without symbol: ${stringifyType(type, checker)}`);
134+
}
135+
return { typeParameters, typeName: symbol.getName() };
136+
}

src/test-utils-new/extractor.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0
33
import ts from 'typescript';
4-
import { extractDeclaration, getDescription, isOptional, stringifyType } from '../components/type-utils';
5-
import { TestUtilsDoc } from '../test-utils/interfaces';
4+
import {
5+
extractDeclaration,
6+
extractTypeArguments,
7+
getDescription,
8+
isNullable,
9+
isOptional,
10+
stringifyType,
11+
} from '../components/type-utils';
12+
import { TestUtilsDoc } from './interfaces';
613

714
function getInheritedFrom(declaration: ts.Declaration, currentClassName: string) {
815
if (!ts.isMethodDeclaration(declaration) || !ts.isClassDeclaration(declaration.parent) || !declaration.parent.name) {
@@ -63,12 +70,23 @@ export default function extractDocumentation(
6370
continue;
6471
}
6572
const type = checker.getTypeAtLocation(declaration);
73+
// report each function signature as a separate method
6674
for (const signature of type.getCallSignatures()) {
67-
// report each function signature as a separate method
75+
const returnType = signature.getReturnType();
76+
// non-nullable type of `void` is `never`
77+
const realReturnType = returnType.flags & ts.TypeFlags.Void ? returnType : returnType.getNonNullableType();
78+
const { typeName, typeParameters } = extractTypeArguments(realReturnType, checker);
79+
6880
classDefinition.methods.push({
6981
name: property.getName(),
7082
description: getDescription(property.getDocumentationComment(checker), declaration).text,
71-
returnType: { name: stringifyType(signature.getReturnType(), checker) },
83+
returnType: {
84+
name: typeName,
85+
isNullable: isNullable(returnType),
86+
typeArguments: typeParameters?.map(typeArgument => ({
87+
name: stringifyType(typeArgument, checker),
88+
})),
89+
},
7290
parameters: signature.parameters.map(parameter => {
7391
const paramType = checker.getTypeAtLocation(extractDeclaration(parameter));
7492
return {

src/test-utils-new/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import fs from 'node:fs';
44
import pathe from 'pathe';
55
import { bootstrapTypescriptProject } from '../bootstrap/typescript';
66
import extractDocumentation from './extractor';
7-
import { TestUtilsDoc } from '../test-utils/interfaces';
7+
import { TestUtilsDoc } from './interfaces';
88

99
export interface TestUtilsVariantOptions {
1010
root: string;

src/test-utils-new/interfaces.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,17 @@ export interface Parameter {
88
defaultValue?: string;
99
}
1010

11+
interface TypeArgument {
12+
name: string;
13+
}
14+
1115
export interface TestUtilMethod {
1216
name: string;
1317
description?: string;
1418
returnType?: {
1519
name: string;
20+
isNullable: boolean;
21+
typeArguments?: Array<TypeArgument>;
1622
};
1723
parameters: Array<Parameter>;
1824
inheritedFrom?: {

test/test-utils/__snapshots__/doc-generation.test.ts.snap

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,24 @@ exports[`Generate documentation > deal with more complex types 1`] = `
2222
"name": "findAll",
2323
"parameters": [],
2424
"returnType": {
25-
"name": "Array<HTMLElement>",
25+
"isNullable": false,
26+
"name": "Array",
27+
"typeArguments": [
28+
{
29+
"name": "HTMLElement",
30+
},
31+
],
32+
},
33+
},
34+
{
35+
"description": undefined,
36+
"inheritedFrom": undefined,
37+
"name": "findSomething",
38+
"parameters": [],
39+
"returnType": {
40+
"isNullable": true,
41+
"name": "TestUtilWrapper",
42+
"typeArguments": undefined,
2643
},
2744
},
2845
{
@@ -41,7 +58,9 @@ exports[`Generate documentation > deal with more complex types 1`] = `
4158
},
4259
],
4360
"returnType": {
61+
"isNullable": false,
4462
"name": "void",
63+
"typeArguments": undefined,
4564
},
4665
},
4766
{
@@ -60,7 +79,9 @@ exports[`Generate documentation > deal with more complex types 1`] = `
6079
},
6180
],
6281
"returnType": {
82+
"isNullable": false,
6383
"name": "void",
84+
"typeArguments": undefined,
6485
},
6586
},
6687
{
@@ -79,7 +100,9 @@ exports[`Generate documentation > deal with more complex types 1`] = `
79100
},
80101
],
81102
"returnType": {
103+
"isNullable": false,
82104
"name": "void",
105+
"typeArguments": undefined,
83106
},
84107
},
85108
]

test/test-utils/doc-generation.test.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,14 @@ describe('Generate documentation', () => {
1919

2020
const noOpMethod = methods.find(method => method.name === 'noOp');
2121
expect(noOpMethod).toBeDefined();
22-
expect(noOpMethod?.returnType).toEqual({ name: 'void' });
22+
expect(noOpMethod?.returnType).toEqual({ name: 'void', isNullable: false });
2323
expect(noOpMethod?.parameters).toEqual([]);
2424
expect(noOpMethod?.description).toBeUndefined();
2525
expect(noOpMethod?.inheritedFrom).toBeUndefined();
2626

2727
const findStringMethod = methods.find(method => method.name === 'findString');
2828
expect(findStringMethod).toBeDefined();
29-
expect(findStringMethod?.returnType).toEqual({ name: 'string' });
29+
expect(findStringMethod?.returnType).toEqual({ name: 'string', isNullable: false });
3030
expect(findStringMethod?.parameters).toEqual([]);
3131
expect(findStringMethod?.description).toBe(
3232
'Finds a string.\n\nThe function may look trivial but people have been losing their words\nsince centuries.'
@@ -35,14 +35,14 @@ describe('Generate documentation', () => {
3535

3636
const setStringMethod = methods.find(method => method.name === 'setString');
3737
expect(setStringMethod).toBeDefined();
38-
expect(setStringMethod?.returnType).toEqual({ name: 'void' });
38+
expect(setStringMethod?.returnType).toEqual({ name: 'void', isNullable: false });
3939
expect(setStringMethod?.parameters).toMatchSnapshot();
4040
expect(setStringMethod?.description).toBe('Short Text');
4141
expect(setStringMethod?.inheritedFrom).toBeUndefined();
4242

4343
const findObjectMethod = methods.find(method => method.name === 'findObject');
4444
expect(findObjectMethod).toBeDefined();
45-
expect(findObjectMethod?.returnType).toEqual({ name: 'TestReturnType' });
45+
expect(findObjectMethod?.returnType).toEqual({ name: 'TestReturnType', isNullable: false });
4646
expect(findObjectMethod?.parameters).toEqual([]);
4747
expect(findObjectMethod?.description).toBe('Short Text.\n\nLong Text.');
4848
expect(findObjectMethod?.inheritedFrom).toBeUndefined();
@@ -57,7 +57,7 @@ describe('Generate documentation', () => {
5757
expect(classDoc.name).toBe('TestUtilWrapper');
5858

5959
const methods = classDoc.methods;
60-
expect(methods.length).toBe(4);
60+
expect(methods.length).toBe(5);
6161
expect(methods).toMatchSnapshot();
6262
});
6363

@@ -85,11 +85,13 @@ describe('Generate documentation', () => {
8585

8686
test('deal with re-exports', () => {
8787
const results = buildTestUtilsProject('exports');
88-
expect(results.map(classDoc => classDoc.name)).toEqual(['AlertWrapper', 'ButtonWrapper']);
88+
expect(results.map(classDoc => classDoc.name)).toEqual(['AlertWrapper', 'ButtonWrapper', 'CardsWrapper']);
8989
const alertWrapper = results.find(classDoc => classDoc.name === 'AlertWrapper')!;
9090
expect(alertWrapper.methods.map(method => method.name)).toEqual(['findContent']);
9191
const buttonWrapper = results.find(classDoc => classDoc.name === 'ButtonWrapper')!;
9292
expect(buttonWrapper.methods.map(method => method.name)).toEqual(['findText']);
93+
const cardsWrapper = results.find(classDoc => classDoc.name === 'CardsWrapper')!;
94+
expect(cardsWrapper.methods.map(method => method.name)).toEqual(['findItems']);
9395
});
9496

9597
test('default value rendering', () => {
@@ -110,7 +112,7 @@ describe('Generate documentation', () => {
110112
defaultValue: "'first'",
111113
},
112114
],
113-
returnType: { name: 'void' },
115+
returnType: { name: 'void', isNullable: false },
114116
},
115117
{
116118
name: 'openDropdown',
@@ -122,7 +124,7 @@ describe('Generate documentation', () => {
122124
defaultValue: 'false',
123125
},
124126
],
125-
returnType: { name: 'void' },
127+
returnType: { name: 'void', isNullable: false },
126128
},
127129
{
128130
name: 'selectOption',
@@ -134,7 +136,7 @@ describe('Generate documentation', () => {
134136
defaultValue: '1',
135137
},
136138
],
137-
returnType: { name: 'void' },
139+
returnType: { name: 'void', isNullable: false },
138140
},
139141
]);
140142
});

test/test-utils/test-helpers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0
33
import { documentTestUtilsNew, TestUtilsVariantOptions } from '../../src/test-utils-new';
4-
import { TestUtilsDoc } from '../../src/test-utils/interfaces';
4+
import { TestUtilsDoc } from '../../src/test-utils-new/interfaces';
55

66
export function buildTestUtilsProject(
77
name: string,

0 commit comments

Comments
 (0)