Skip to content

resolve TODOS and fix handling of some TypeScript typeof types #747

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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