Skip to content

Commit 1aa0249

Browse files
authored
resolve TODOS and fix handling of some TypeScript typeof types (#747)
1 parent 308eb27 commit 1aa0249

File tree

10 files changed

+213
-63
lines changed

10 files changed

+213
-63
lines changed

.changeset/smart-files-tell.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'react-docgen': patch
3+
---
4+
5+
Handle `typeof import('...')` and `typeof MyType.property` correctly in
6+
TypeScript

packages/react-docgen/src/handlers/codeTypeHandler.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ function setPropDescriptor(
3737
return;
3838
}
3939

40-
// TODO what about other types here
4140
const id = argument.get('id') as NodePath;
4241

4342
if (!id.hasNode() || !id.isIdentifier()) {

packages/react-docgen/src/utils/__tests__/__snapshots__/getTSType-test.ts.snap

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -835,7 +835,13 @@ exports[`getTSType > handles self-referencing type cycles 1`] = `
835835
}
836836
`;
837837
838-
exports[`getTSType > handles typeof types 1`] = `
838+
exports[`getTSType > handles typeof qualified type 1`] = `
839+
{
840+
"name": "MyType.a",
841+
}
842+
`;
843+
844+
exports[`getTSType > handles typeof type 1`] = `
839845
{
840846
"name": "signature",
841847
"raw": "{ a: string, b: xyz }",
@@ -1385,7 +1391,13 @@ exports[`getTSType > resolves keyof with inline object to union 1`] = `
13851391
}
13861392
`;
13871393
1388-
exports[`getTSType > resolves typeof of imported types 1`] = `
1394+
exports[`getTSType > resolves typeof of import type 1`] = `
1395+
{
1396+
"name": "import('MyType')",
1397+
}
1398+
`;
1399+
1400+
exports[`getTSType > resolves typeof of imported type 1`] = `
13891401
{
13901402
"name": "signature",
13911403
"raw": "{ a: number, b: xyz }",
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Vitest Snapshot v1
2+
3+
exports[`getTypeParameters > Flow > detects simple type 1`] = `
4+
{
5+
"T": Node {
6+
"id": Node {
7+
"name": "T",
8+
"type": "Identifier",
9+
},
10+
"type": "GenericTypeAnnotation",
11+
"typeParameters": null,
12+
},
13+
}
14+
`;
15+
16+
exports[`getTypeParameters > TypeScript > detects default 1`] = `
17+
{
18+
"R": Node {
19+
"type": "TSStringKeyword",
20+
},
21+
"T": Node {
22+
"type": "TSTypeReference",
23+
"typeName": Node {
24+
"name": "T",
25+
"type": "Identifier",
26+
},
27+
},
28+
}
29+
`;
30+
31+
exports[`getTypeParameters > TypeScript > detects simple type 1`] = `
32+
{
33+
"T": Node {
34+
"type": "TSTypeReference",
35+
"typeName": Node {
36+
"name": "T",
37+
"type": "Identifier",
38+
},
39+
},
40+
}
41+
`;

packages/react-docgen/src/utils/__tests__/getTSType-test.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -461,7 +461,7 @@ describe('getTSType', () => {
461461
expect(getTSType(typePath)).toMatchSnapshot();
462462
});
463463

464-
test('handles typeof types', () => {
464+
test('handles typeof type', () => {
465465
const typePath = typeAlias(`
466466
var x: typeof MyType = {};
467467
@@ -471,7 +471,17 @@ describe('getTSType', () => {
471471
expect(getTSType(typePath)).toMatchSnapshot();
472472
});
473473

474-
test('resolves typeof of imported types', () => {
474+
test('handles typeof qualified type', () => {
475+
const typePath = typeAlias(`
476+
var x: typeof MyType.a = {};
477+
478+
type MyType = { a: string, b: xyz };
479+
`);
480+
481+
expect(getTSType(typePath)).toMatchSnapshot();
482+
});
483+
484+
test('resolves typeof of imported type', () => {
475485
const typePath = typeAlias(
476486
`
477487
var x: typeof MyType = {};
@@ -483,6 +493,15 @@ describe('getTSType', () => {
483493
expect(getTSType(typePath)).toMatchSnapshot();
484494
});
485495

496+
test('resolves typeof of import type', () => {
497+
const typePath = typeAlias(
498+
"var x: typeof import('MyType') = {};",
499+
mockImporter,
500+
);
501+
502+
expect(getTSType(typePath)).toMatchSnapshot();
503+
});
504+
486505
test('handles qualified type identifiers', () => {
487506
const typePath = typeAlias(`
488507
var x: MyType.x = {};
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import type {
2+
TSTypeAliasDeclaration,
3+
TSTypeParameterDeclaration,
4+
TSTypeParameterInstantiation,
5+
TypeAlias,
6+
TypeParameterDeclaration,
7+
TypeParameterInstantiation,
8+
} from '@babel/types';
9+
import { parse, parseTypescript } from '../../../tests/utils';
10+
import getTypeParameters from '../getTypeParameters.js';
11+
import { describe, expect, test } from 'vitest';
12+
import type { NodePath } from '@babel/traverse';
13+
14+
describe('getTypeParameters', () => {
15+
describe('TypeScript', () => {
16+
test('detects simple type', () => {
17+
const path =
18+
parseTypescript.statement<TSTypeAliasDeclaration>('type x<T> = y<T>');
19+
20+
expect(
21+
getTypeParameters(
22+
path.get('typeParameters') as NodePath<TSTypeParameterDeclaration>,
23+
path
24+
.get('typeAnnotation')
25+
.get('typeParameters') as NodePath<TSTypeParameterInstantiation>,
26+
null,
27+
),
28+
).toMatchSnapshot();
29+
});
30+
test('detects default', () => {
31+
const path = parseTypescript.statement<TSTypeAliasDeclaration>(
32+
'type x<T, R = string> = y<T>;',
33+
);
34+
35+
expect(
36+
getTypeParameters(
37+
path.get('typeParameters') as NodePath<TSTypeParameterDeclaration>,
38+
path
39+
.get('typeAnnotation')
40+
.get('typeParameters') as NodePath<TSTypeParameterInstantiation>,
41+
null,
42+
),
43+
).toMatchSnapshot();
44+
});
45+
});
46+
describe('Flow', () => {
47+
test('detects simple type', () => {
48+
const path = parse.statement<TypeAlias>('type x<T> = y<T>');
49+
50+
expect(
51+
getTypeParameters(
52+
path.get('typeParameters') as NodePath<TypeParameterDeclaration>,
53+
path
54+
.get('right')
55+
.get('typeParameters') as NodePath<TypeParameterInstantiation>,
56+
null,
57+
),
58+
).toMatchSnapshot();
59+
});
60+
});
61+
});

packages/react-docgen/src/utils/getTSType.ts

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import type {
3535
TSTypeParameterDeclaration,
3636
RestElement,
3737
TypeScript,
38+
TSQualifiedName,
3839
} from '@babel/types';
3940
import { getDocblock } from './docblock.js';
4041

@@ -68,6 +69,22 @@ const namedTypes = {
6869
TSIndexedAccessType: handleTSIndexedAccessType,
6970
};
7071

72+
function handleTSQualifiedName(
73+
path: NodePath<TSQualifiedName>,
74+
): TypeDescriptor<TSFunctionSignatureType> {
75+
const left = path.get('left');
76+
const right = path.get('right');
77+
78+
if (left.isIdentifier({ name: 'React' }) && right.isIdentifier()) {
79+
return {
80+
name: `${left.node.name}${right.node.name}`,
81+
raw: printValue(path),
82+
};
83+
}
84+
85+
return { name: printValue(path).replace(/<.*>$/, '') };
86+
}
87+
7188
function handleTSArrayType(
7289
path: NodePath<TSArrayType>,
7390
typeParams: TypeParameters | null,
@@ -87,17 +104,7 @@ function handleTSTypeReference(
87104
const typeName = path.get('typeName');
88105

89106
if (typeName.isTSQualifiedName()) {
90-
const left = typeName.get('left');
91-
const right = typeName.get('right');
92-
93-
if (left.isIdentifier({ name: 'React' }) && right.isIdentifier()) {
94-
type = {
95-
name: `${left.node.name}${right.node.name}`,
96-
raw: printValue(typeName),
97-
};
98-
} else {
99-
type = { name: printValue(typeName).replace(/<.*>$/, '') };
100-
}
107+
type = handleTSQualifiedName(typeName);
101108
} else {
102109
type = { name: (typeName as NodePath<Identifier>).node.name };
103110
}
@@ -366,17 +373,25 @@ function handleTSTypeQuery(
366373
path: NodePath<TSTypeQuery>,
367374
typeParams: TypeParameters | null,
368375
): TypeDescriptor<TSFunctionSignatureType> {
369-
const resolvedPath = resolveToValue(path.get('exprName'));
376+
const exprName = path.get('exprName');
370377

371-
if ('typeAnnotation' in resolvedPath.node) {
372-
return getTSTypeWithResolvedTypes(
373-
resolvedPath.get('typeAnnotation') as NodePath<TypeScript>,
374-
typeParams,
375-
);
376-
}
378+
if (exprName.isIdentifier()) {
379+
const resolvedPath = resolveToValue(path.get('exprName'));
380+
381+
if (resolvedPath.has('typeAnnotation')) {
382+
return getTSTypeWithResolvedTypes(
383+
resolvedPath.get('typeAnnotation') as NodePath<TypeScript>,
384+
typeParams,
385+
);
386+
}
377387

378-
// @ts-ignore Do we need to handle TsQualifiedName here TODO
379-
return { name: path.node.exprName.name };
388+
return { name: exprName.node.name };
389+
} else if (exprName.isTSQualifiedName()) {
390+
return handleTSQualifiedName(exprName);
391+
} else {
392+
// TSImportType
393+
return { name: printValue(exprName) };
394+
}
380395
}
381396

382397
function handleTSTypeOperator(
Lines changed: 34 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
11
import resolveGenericTypeAnnotation from '../utils/resolveGenericTypeAnnotation.js';
22
import type { NodePath } from '@babel/traverse';
33
import type {
4-
FlowType,
54
Identifier,
65
QualifiedTypeIdentifier,
76
TSQualifiedName,
7+
TSTypeParameter,
88
TSTypeParameterDeclaration,
99
TSTypeParameterInstantiation,
10+
TypeParameter,
1011
TypeParameterDeclaration,
1112
TypeParameterInstantiation,
1213
} from '@babel/types';
1314

14-
// TODO needs tests TS && flow
15-
1615
export type TypeParameters = Record<string, NodePath>;
1716

1817
export default function getTypeParameters(
@@ -27,41 +26,42 @@ export default function getTypeParameters(
2726

2827
let i = 0;
2928

30-
declaration.get('params').forEach((paramPath) => {
31-
const key = paramPath.node.name;
32-
const defaultTypePath = paramPath.node.default
33-
? (paramPath.get('default') as NodePath<FlowType>)
34-
: null;
35-
const typePath =
36-
i < numInstantiationParams
37-
? instantiation.get('params')[i++]
38-
: defaultTypePath;
29+
declaration
30+
.get('params')
31+
.forEach((paramPath: NodePath<TSTypeParameter | TypeParameter>) => {
32+
const key = paramPath.node.name;
33+
const defaultProp = paramPath.get('default');
34+
const defaultTypePath = defaultProp.hasNode() ? defaultProp : null;
35+
const typePath =
36+
i < numInstantiationParams
37+
? instantiation.get('params')[i++]
38+
: defaultTypePath;
3939

40-
if (typePath) {
41-
let resolvedTypePath: NodePath =
42-
resolveGenericTypeAnnotation(typePath) || typePath;
43-
let typeName:
44-
| NodePath<Identifier | QualifiedTypeIdentifier | TSQualifiedName>
45-
| undefined;
40+
if (typePath) {
41+
let resolvedTypePath: NodePath =
42+
resolveGenericTypeAnnotation(typePath) || typePath;
43+
let typeName:
44+
| NodePath<Identifier | QualifiedTypeIdentifier | TSQualifiedName>
45+
| undefined;
4646

47-
if (resolvedTypePath.isTSTypeReference()) {
48-
typeName = resolvedTypePath.get('typeName');
49-
} else if (resolvedTypePath.isGenericTypeAnnotation()) {
50-
typeName = resolvedTypePath.get('id');
51-
}
47+
if (resolvedTypePath.isTSTypeReference()) {
48+
typeName = resolvedTypePath.get('typeName');
49+
} else if (resolvedTypePath.isGenericTypeAnnotation()) {
50+
typeName = resolvedTypePath.get('id');
51+
}
5252

53-
if (
54-
typeName &&
55-
inputParams &&
56-
typeName.isIdentifier() &&
57-
inputParams[typeName.node.name]
58-
) {
59-
resolvedTypePath = inputParams[typeName.node.name];
60-
}
53+
if (
54+
typeName &&
55+
inputParams &&
56+
typeName.isIdentifier() &&
57+
inputParams[typeName.node.name]
58+
) {
59+
resolvedTypePath = inputParams[typeName.node.name];
60+
}
6161

62-
params[key] = resolvedTypePath;
63-
}
64-
});
62+
params[key] = resolvedTypePath;
63+
}
64+
});
6565

6666
return params;
6767
}

packages/react-docgen/src/utils/resolveFunctionDefinitionToReturnValue.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,6 @@ const explodedVisitors = visitors.explode<TraverseState>({
2727
},
2828
});
2929

30-
// TODO needs unit test
31-
3230
export default function resolveFunctionDefinitionToReturnValue(
3331
path: NodePath<BabelFunction>,
3432
): NodePath<Expression> | null {

packages/react-docgen/src/utils/resolveToValue.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ function findScopePath(
2121
resolvedParentPath.isImportDefaultSpecifier() ||
2222
resolvedParentPath.isImportSpecifier()
2323
) {
24-
// TODO TESTME
2524
let exportName: string | undefined;
2625

2726
if (resolvedParentPath.isImportDefaultSpecifier()) {
@@ -184,7 +183,7 @@ export default function resolveToValue(path: NodePath): NodePath {
184183
if (property.isIdentifier() || property.isStringLiteral()) {
185184
const memberPath = getMemberValuePath(
186185
resolved,
187-
property.isIdentifier() ? property.node.name : property.node.value, // TODO TESTME
186+
property.isIdentifier() ? property.node.name : property.node.value,
188187
);
189188

190189
if (memberPath) {

0 commit comments

Comments
 (0)