Skip to content

Commit f1d4104

Browse files
authored
chore: support dependency types (#89)
1 parent 5d07db2 commit f1d4104

File tree

4 files changed

+117
-51
lines changed

4 files changed

+117
-51
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
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+
export class DropdownWrapper extends ElementWrapper {
7+
findItems(): Array<ElementWrapper> {
8+
return [];
9+
}
10+
11+
// test for circular dependency
12+
findItemGroup() {
13+
return new DropdownWrapper();
14+
}
15+
}

fixtures/test-utils/exports/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
export { AlertWrapper } from './alert';
44
export { ButtonWrapper } from './button';
55
export { CardsWrapper } from './cards';
6+
export { DropdownWrapper } from './dropdown';

src/test-utils-new/extractor.ts

Lines changed: 90 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -43,68 +43,108 @@ export default function extractDocumentation(
4343
}
4444

4545
const exportSymbols = checker.getExportsOfModule(moduleSymbol);
46-
const definitions: Array<TestUtilsDoc> = [];
46+
const definitions = new Map<string, TestUtilsDoc>();
4747

4848
for (const symbol of exportSymbols) {
4949
const className = symbol.getName();
5050
if (extraExports.includes(className)) {
5151
continue;
5252
}
5353
const classType = checker.getDeclaredTypeOfSymbol(symbol);
54-
if (!classType.isClass()) {
55-
throw new Error(`Exported symbol is not a class, got ${checker.symbolToString(symbol)}`);
56-
}
54+
documentClass(definitions, symbol, classType, checker);
55+
}
5756

58-
const classDefinition: TestUtilsDoc = { name: className, methods: [] };
59-
for (const property of classType.getProperties()) {
60-
const declaration = property.valueDeclaration;
61-
if (!declaration) {
62-
throw new Error(`Unexpected member on ${className}${property.getName()}`);
63-
}
64-
const modifiers = (ts.canHaveModifiers(declaration) && ts.getModifiers(declaration)) || [];
65-
if (
66-
modifiers.find(
67-
modifier => modifier.kind & ts.SyntaxKind.ProtectedKeyword || modifier.kind & ts.SyntaxKind.PrivateKeyword
68-
)
69-
) {
70-
continue;
71-
}
72-
const type = checker.getTypeAtLocation(declaration);
73-
// report each function signature as a separate method
74-
for (const signature of type.getCallSignatures()) {
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);
57+
return Array.from(definitions.values());
58+
}
7959

80-
classDefinition.methods.push({
81-
name: property.getName(),
82-
description: getDescription(property.getDocumentationComment(checker), declaration).text,
83-
returnType: {
84-
name: typeName,
85-
isNullable: isNullable(returnType),
86-
typeArguments: typeParameters?.map(typeArgument => ({
87-
name: stringifyType(typeArgument, checker),
88-
})),
89-
},
90-
parameters: signature.parameters.map(parameter => {
91-
const paramType = checker.getTypeAtLocation(extractDeclaration(parameter));
92-
return {
93-
name: parameter.name,
94-
typeName: stringifyType(paramType, checker),
95-
description: getDescription(parameter.getDocumentationComment(checker), declaration).text,
96-
flags: { isOptional: isOptional(paramType) },
97-
defaultValue: getDefaultValue(extractDeclaration(parameter)),
98-
};
99-
}),
100-
inheritedFrom: getInheritedFrom(declaration, className),
101-
});
60+
function documentClass(
61+
definitions: Map<string, TestUtilsDoc>,
62+
symbol: ts.Symbol,
63+
classType: ts.Type,
64+
checker: ts.TypeChecker
65+
) {
66+
if (!classType.isClass()) {
67+
throw new Error(`Exported symbol is not a class, got ${checker.symbolToString(symbol)}`);
68+
}
69+
const className = symbol.getName();
70+
const definition: TestUtilsDoc = { name: className, methods: [] };
71+
definitions.set(className, definition);
72+
73+
for (const property of classType.getProperties()) {
74+
const declaration = property.valueDeclaration;
75+
if (!declaration) {
76+
throw new Error(`Unexpected member on ${className}${property.getName()}`);
77+
}
78+
const modifiers = (ts.canHaveModifiers(declaration) && ts.getModifiers(declaration)) || [];
79+
if (
80+
modifiers.find(
81+
modifier => modifier.kind & ts.SyntaxKind.ProtectedKeyword || modifier.kind & ts.SyntaxKind.PrivateKeyword
82+
)
83+
) {
84+
continue;
85+
}
86+
const type = checker.getTypeAtLocation(declaration);
87+
// report each function signature as a separate method
88+
for (const signature of type.getCallSignatures()) {
89+
const maybeReturnType = signature.getReturnType();
90+
// non-nullable type of `void` is `never`
91+
const returnType =
92+
maybeReturnType.flags & ts.TypeFlags.Void ? maybeReturnType : maybeReturnType.getNonNullableType();
93+
const dependency = findDependencyType(returnType, checker);
94+
if (dependency && !definitions.has(dependency.symbol.getName())) {
95+
documentClass(definitions, dependency.symbol, dependency.type, checker);
10296
}
97+
98+
const { typeName, typeParameters } = extractTypeArguments(returnType, checker);
99+
100+
definition.methods.push({
101+
name: property.getName(),
102+
description: getDescription(property.getDocumentationComment(checker), declaration).text,
103+
returnType: {
104+
name: typeName,
105+
isNullable: isNullable(maybeReturnType),
106+
typeArguments: typeParameters?.map(typeArgument => ({
107+
name: stringifyType(typeArgument, checker),
108+
})),
109+
},
110+
parameters: signature.parameters.map(parameter => {
111+
const paramType = checker.getTypeAtLocation(extractDeclaration(parameter));
112+
return {
113+
name: parameter.name,
114+
typeName: stringifyType(paramType, checker),
115+
description: getDescription(parameter.getDocumentationComment(checker), declaration).text,
116+
flags: { isOptional: isOptional(paramType) },
117+
defaultValue: getDefaultValue(extractDeclaration(parameter)),
118+
};
119+
}),
120+
inheritedFrom: getInheritedFrom(declaration, className),
121+
});
103122
}
104-
classDefinition.methods.sort((a, b) => a.name.localeCompare(b.name));
123+
}
124+
definition.methods.sort((a, b) => a.name.localeCompare(b.name));
125+
}
126+
127+
function findDependencyType(type: ts.Type, checker: ts.TypeChecker): { type: ts.Type; symbol: ts.Symbol } | undefined {
128+
const symbol = type.getSymbol();
129+
if (!symbol) {
130+
return;
131+
}
105132

106-
definitions.push(classDefinition);
133+
const typeName = symbol.getName();
134+
if (typeName === 'Array') {
135+
const itemType = checker.getTypeArguments(type as ts.TypeReference)[0];
136+
return findDependencyType(itemType, checker);
137+
}
138+
if (
139+
!typeName.endsWith('Wrapper') ||
140+
['ElementWrapper', 'ComponentWrapper'].includes(typeName) ||
141+
!type.isClassOrInterface()
142+
) {
143+
return;
107144
}
108145

109-
return definitions;
146+
return {
147+
type,
148+
symbol,
149+
};
110150
}

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,13 +85,23 @@ 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', 'CardsWrapper']);
88+
expect(results.map(classDoc => classDoc.name)).toEqual([
89+
'AlertWrapper',
90+
'ButtonWrapper',
91+
'CardsWrapper',
92+
'CardWrapper',
93+
'DropdownWrapper',
94+
]);
8995
const alertWrapper = results.find(classDoc => classDoc.name === 'AlertWrapper')!;
9096
expect(alertWrapper.methods.map(method => method.name)).toEqual(['findContent']);
9197
const buttonWrapper = results.find(classDoc => classDoc.name === 'ButtonWrapper')!;
9298
expect(buttonWrapper.methods.map(method => method.name)).toEqual(['findText']);
9399
const cardsWrapper = results.find(classDoc => classDoc.name === 'CardsWrapper')!;
94100
expect(cardsWrapper.methods.map(method => method.name)).toEqual(['findItems']);
101+
const cardItemWrapper = results.find(classDoc => classDoc.name === 'CardWrapper')!;
102+
expect(cardItemWrapper.methods.map(method => method.name)).toEqual(['findContent', 'findHeader']);
103+
const dropdownWrapper = results.find(classDoc => classDoc.name === 'DropdownWrapper')!;
104+
expect(dropdownWrapper.methods.map(method => method.name)).toEqual(['findItemGroup', 'findItems']);
95105
});
96106

97107
test('default value rendering', () => {

0 commit comments

Comments
 (0)