Skip to content

Commit

Permalink
resolve TODOS and fix handling of some TypeScript typeof types (#747)
Browse files Browse the repository at this point in the history
  • Loading branch information
danez authored Jan 28, 2023
1 parent 308eb27 commit 1aa0249
Show file tree
Hide file tree
Showing 10 changed files with 213 additions and 63 deletions.
6 changes: 6 additions & 0 deletions .changeset/smart-files-tell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'react-docgen': patch
---

Handle `typeof import('...')` and `typeof MyType.property` correctly in
TypeScript
1 change: 0 additions & 1 deletion packages/react-docgen/src/handlers/codeTypeHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ function setPropDescriptor(
return;
}

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

if (!id.hasNode() || !id.isIdentifier()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -835,7 +835,13 @@ exports[`getTSType > handles self-referencing type cycles 1`] = `
}
`;
exports[`getTSType > handles typeof types 1`] = `
exports[`getTSType > handles typeof qualified type 1`] = `
{
"name": "MyType.a",
}
`;
exports[`getTSType > handles typeof type 1`] = `
{
"name": "signature",
"raw": "{ a: string, b: xyz }",
Expand Down Expand Up @@ -1385,7 +1391,13 @@ exports[`getTSType > resolves keyof with inline object to union 1`] = `
}
`;
exports[`getTSType > resolves typeof of imported types 1`] = `
exports[`getTSType > resolves typeof of import type 1`] = `
{
"name": "import('MyType')",
}
`;
exports[`getTSType > resolves typeof of imported type 1`] = `
{
"name": "signature",
"raw": "{ a: number, b: xyz }",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Vitest Snapshot v1

exports[`getTypeParameters > Flow > detects simple type 1`] = `
{
"T": Node {
"id": Node {
"name": "T",
"type": "Identifier",
},
"type": "GenericTypeAnnotation",
"typeParameters": null,
},
}
`;

exports[`getTypeParameters > TypeScript > detects default 1`] = `
{
"R": Node {
"type": "TSStringKeyword",
},
"T": Node {
"type": "TSTypeReference",
"typeName": Node {
"name": "T",
"type": "Identifier",
},
},
}
`;

exports[`getTypeParameters > TypeScript > detects simple type 1`] = `
{
"T": Node {
"type": "TSTypeReference",
"typeName": Node {
"name": "T",
"type": "Identifier",
},
},
}
`;
23 changes: 21 additions & 2 deletions packages/react-docgen/src/utils/__tests__/getTSType-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -461,7 +461,7 @@ describe('getTSType', () => {
expect(getTSType(typePath)).toMatchSnapshot();
});

test('handles typeof types', () => {
test('handles typeof type', () => {
const typePath = typeAlias(`
var x: typeof MyType = {};
Expand All @@ -471,7 +471,17 @@ describe('getTSType', () => {
expect(getTSType(typePath)).toMatchSnapshot();
});

test('resolves typeof of imported types', () => {
test('handles typeof qualified type', () => {
const typePath = typeAlias(`
var x: typeof MyType.a = {};
type MyType = { a: string, b: xyz };
`);

expect(getTSType(typePath)).toMatchSnapshot();
});

test('resolves typeof of imported type', () => {
const typePath = typeAlias(
`
var x: typeof MyType = {};
Expand All @@ -483,6 +493,15 @@ describe('getTSType', () => {
expect(getTSType(typePath)).toMatchSnapshot();
});

test('resolves typeof of import type', () => {
const typePath = typeAlias(
"var x: typeof import('MyType') = {};",
mockImporter,
);

expect(getTSType(typePath)).toMatchSnapshot();
});

test('handles qualified type identifiers', () => {
const typePath = typeAlias(`
var x: MyType.x = {};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import type {
TSTypeAliasDeclaration,
TSTypeParameterDeclaration,
TSTypeParameterInstantiation,
TypeAlias,
TypeParameterDeclaration,
TypeParameterInstantiation,
} from '@babel/types';
import { parse, parseTypescript } from '../../../tests/utils';
import getTypeParameters from '../getTypeParameters.js';
import { describe, expect, test } from 'vitest';
import type { NodePath } from '@babel/traverse';

describe('getTypeParameters', () => {
describe('TypeScript', () => {
test('detects simple type', () => {
const path =
parseTypescript.statement<TSTypeAliasDeclaration>('type x<T> = y<T>');

expect(
getTypeParameters(
path.get('typeParameters') as NodePath<TSTypeParameterDeclaration>,
path
.get('typeAnnotation')
.get('typeParameters') as NodePath<TSTypeParameterInstantiation>,
null,
),
).toMatchSnapshot();
});
test('detects default', () => {
const path = parseTypescript.statement<TSTypeAliasDeclaration>(
'type x<T, R = string> = y<T>;',
);

expect(
getTypeParameters(
path.get('typeParameters') as NodePath<TSTypeParameterDeclaration>,
path
.get('typeAnnotation')
.get('typeParameters') as NodePath<TSTypeParameterInstantiation>,
null,
),
).toMatchSnapshot();
});
});
describe('Flow', () => {
test('detects simple type', () => {
const path = parse.statement<TypeAlias>('type x<T> = y<T>');

expect(
getTypeParameters(
path.get('typeParameters') as NodePath<TypeParameterDeclaration>,
path
.get('right')
.get('typeParameters') as NodePath<TypeParameterInstantiation>,
null,
),
).toMatchSnapshot();
});
});
});
55 changes: 35 additions & 20 deletions packages/react-docgen/src/utils/getTSType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import type {
TSTypeParameterDeclaration,
RestElement,
TypeScript,
TSQualifiedName,
} from '@babel/types';
import { getDocblock } from './docblock.js';

Expand Down Expand Up @@ -68,6 +69,22 @@ const namedTypes = {
TSIndexedAccessType: handleTSIndexedAccessType,
};

function handleTSQualifiedName(
path: NodePath<TSQualifiedName>,
): TypeDescriptor<TSFunctionSignatureType> {
const left = path.get('left');
const right = path.get('right');

if (left.isIdentifier({ name: 'React' }) && right.isIdentifier()) {
return {
name: `${left.node.name}${right.node.name}`,
raw: printValue(path),
};
}

return { name: printValue(path).replace(/<.*>$/, '') };
}

function handleTSArrayType(
path: NodePath<TSArrayType>,
typeParams: TypeParameters | null,
Expand All @@ -87,17 +104,7 @@ function handleTSTypeReference(
const typeName = path.get('typeName');

if (typeName.isTSQualifiedName()) {
const left = typeName.get('left');
const right = typeName.get('right');

if (left.isIdentifier({ name: 'React' }) && right.isIdentifier()) {
type = {
name: `${left.node.name}${right.node.name}`,
raw: printValue(typeName),
};
} else {
type = { name: printValue(typeName).replace(/<.*>$/, '') };
}
type = handleTSQualifiedName(typeName);
} else {
type = { name: (typeName as NodePath<Identifier>).node.name };
}
Expand Down Expand Up @@ -366,17 +373,25 @@ function handleTSTypeQuery(
path: NodePath<TSTypeQuery>,
typeParams: TypeParameters | null,
): TypeDescriptor<TSFunctionSignatureType> {
const resolvedPath = resolveToValue(path.get('exprName'));
const exprName = path.get('exprName');

if ('typeAnnotation' in resolvedPath.node) {
return getTSTypeWithResolvedTypes(
resolvedPath.get('typeAnnotation') as NodePath<TypeScript>,
typeParams,
);
}
if (exprName.isIdentifier()) {
const resolvedPath = resolveToValue(path.get('exprName'));

if (resolvedPath.has('typeAnnotation')) {
return getTSTypeWithResolvedTypes(
resolvedPath.get('typeAnnotation') as NodePath<TypeScript>,
typeParams,
);
}

// @ts-ignore Do we need to handle TsQualifiedName here TODO
return { name: path.node.exprName.name };
return { name: exprName.node.name };
} else if (exprName.isTSQualifiedName()) {
return handleTSQualifiedName(exprName);
} else {
// TSImportType
return { name: printValue(exprName) };
}
}

function handleTSTypeOperator(
Expand Down
68 changes: 34 additions & 34 deletions packages/react-docgen/src/utils/getTypeParameters.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
import resolveGenericTypeAnnotation from '../utils/resolveGenericTypeAnnotation.js';
import type { NodePath } from '@babel/traverse';
import type {
FlowType,
Identifier,
QualifiedTypeIdentifier,
TSQualifiedName,
TSTypeParameter,
TSTypeParameterDeclaration,
TSTypeParameterInstantiation,
TypeParameter,
TypeParameterDeclaration,
TypeParameterInstantiation,
} from '@babel/types';

// TODO needs tests TS && flow

export type TypeParameters = Record<string, NodePath>;

export default function getTypeParameters(
Expand All @@ -27,41 +26,42 @@ export default function getTypeParameters(

let i = 0;

declaration.get('params').forEach((paramPath) => {
const key = paramPath.node.name;
const defaultTypePath = paramPath.node.default
? (paramPath.get('default') as NodePath<FlowType>)
: null;
const typePath =
i < numInstantiationParams
? instantiation.get('params')[i++]
: defaultTypePath;
declaration
.get('params')
.forEach((paramPath: NodePath<TSTypeParameter | TypeParameter>) => {
const key = paramPath.node.name;
const defaultProp = paramPath.get('default');
const defaultTypePath = defaultProp.hasNode() ? defaultProp : null;
const typePath =
i < numInstantiationParams
? instantiation.get('params')[i++]
: defaultTypePath;

if (typePath) {
let resolvedTypePath: NodePath =
resolveGenericTypeAnnotation(typePath) || typePath;
let typeName:
| NodePath<Identifier | QualifiedTypeIdentifier | TSQualifiedName>
| undefined;
if (typePath) {
let resolvedTypePath: NodePath =
resolveGenericTypeAnnotation(typePath) || typePath;
let typeName:
| NodePath<Identifier | QualifiedTypeIdentifier | TSQualifiedName>
| undefined;

if (resolvedTypePath.isTSTypeReference()) {
typeName = resolvedTypePath.get('typeName');
} else if (resolvedTypePath.isGenericTypeAnnotation()) {
typeName = resolvedTypePath.get('id');
}
if (resolvedTypePath.isTSTypeReference()) {
typeName = resolvedTypePath.get('typeName');
} else if (resolvedTypePath.isGenericTypeAnnotation()) {
typeName = resolvedTypePath.get('id');
}

if (
typeName &&
inputParams &&
typeName.isIdentifier() &&
inputParams[typeName.node.name]
) {
resolvedTypePath = inputParams[typeName.node.name];
}
if (
typeName &&
inputParams &&
typeName.isIdentifier() &&
inputParams[typeName.node.name]
) {
resolvedTypePath = inputParams[typeName.node.name];
}

params[key] = resolvedTypePath;
}
});
params[key] = resolvedTypePath;
}
});

return params;
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,6 @@ const explodedVisitors = visitors.explode<TraverseState>({
},
});

// TODO needs unit test

export default function resolveFunctionDefinitionToReturnValue(
path: NodePath<BabelFunction>,
): NodePath<Expression> | null {
Expand Down
3 changes: 1 addition & 2 deletions packages/react-docgen/src/utils/resolveToValue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ function findScopePath(
resolvedParentPath.isImportDefaultSpecifier() ||
resolvedParentPath.isImportSpecifier()
) {
// TODO TESTME
let exportName: string | undefined;

if (resolvedParentPath.isImportDefaultSpecifier()) {
Expand Down Expand Up @@ -184,7 +183,7 @@ export default function resolveToValue(path: NodePath): NodePath {
if (property.isIdentifier() || property.isStringLiteral()) {
const memberPath = getMemberValuePath(
resolved,
property.isIdentifier() ? property.node.name : property.node.value, // TODO TESTME
property.isIdentifier() ? property.node.name : property.node.value,
);

if (memberPath) {
Expand Down

0 comments on commit 1aa0249

Please sign in to comment.