Skip to content

Commit 0c1b9c8

Browse files
committed
Detect @typedef re-exports
See #2044
1 parent 71fb913 commit 0c1b9c8

File tree

10 files changed

+118
-6
lines changed

10 files changed

+118
-6
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Unreleased
22

3+
### Features
4+
5+
- TypeDoc will now treat `@typedef {import("foo").Bar<Z>} Baz` type declarations which forward type parameters to the imported
6+
symbol as re-exports of that symbol, #2044.
7+
38
## v0.23.14 (2022-09-03)
49

510
### Features

src/lib/converter/jsdoc.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
convertParameterNodes,
1919
convertTypeParameterNodes,
2020
} from "./factories/signature";
21+
import { convertSymbol } from "./symbols";
2122

2223
export function convertJsDocAlias(
2324
context: Context,
@@ -33,6 +34,15 @@ export function convertJsDocAlias(
3334
return;
3435
}
3536

37+
// If the typedef tag is just referring to another type-space symbol, with no type parameters
38+
// or appropriate forwarding type parameters, then we treat it as a re-export instead of creating
39+
// a type alias with an import type.
40+
const aliasedSymbol = getTypedefReExportTarget(context, declaration);
41+
if (aliasedSymbol) {
42+
convertSymbol(context, aliasedSymbol, exportSymbol ?? symbol);
43+
return;
44+
}
45+
3646
const reflection = context.createDeclarationReflection(
3747
ReflectionKind.TypeAlias,
3848
symbol,
@@ -165,3 +175,58 @@ function convertTemplateParameterNodes(
165175
const params = (nodes ?? []).flatMap((tag) => tag.typeParameters);
166176
return convertTypeParameterNodes(context, params);
167177
}
178+
179+
function getTypedefReExportTarget(
180+
context: Context,
181+
declaration: ts.JSDocTypedefTag | ts.JSDocEnumTag
182+
): ts.Symbol | undefined {
183+
const typeExpression = declaration.typeExpression;
184+
if (
185+
!ts.isJSDocTypedefTag(declaration) ||
186+
!typeExpression ||
187+
ts.isJSDocTypeLiteral(typeExpression) ||
188+
!ts.isImportTypeNode(typeExpression.type) ||
189+
!typeExpression.type.qualifier ||
190+
!ts.isIdentifier(typeExpression.type.qualifier)
191+
) {
192+
return;
193+
}
194+
195+
const targetSymbol = context.expectSymbolAtLocation(
196+
typeExpression.type.qualifier
197+
);
198+
const decl = targetSymbol.declarations?.[0];
199+
200+
if (
201+
!decl ||
202+
!(
203+
ts.isTypeAliasDeclaration(decl) ||
204+
ts.isInterfaceDeclaration(decl) ||
205+
ts.isJSDocTypedefTag(decl) ||
206+
ts.isJSDocCallbackTag(decl)
207+
)
208+
) {
209+
return;
210+
}
211+
212+
const targetParams = ts.getEffectiveTypeParameterDeclarations(decl);
213+
const localParams = ts.getEffectiveTypeParameterDeclarations(declaration);
214+
const localArgs = typeExpression.type.typeArguments || [];
215+
216+
// If we have type parameters, ensure they are forwarding parameters with no transformations.
217+
// This doesn't check constraints since they aren't checked in JSDoc types.
218+
if (
219+
targetParams.length !== localParams.length ||
220+
localArgs.some(
221+
(arg, i) =>
222+
!ts.isTypeReferenceNode(arg) ||
223+
!ts.isIdentifier(arg.typeName) ||
224+
arg.typeArguments ||
225+
localParams[i]?.name.text !== arg.typeName.text
226+
)
227+
) {
228+
return;
229+
}
230+
231+
return targetSymbol;
232+
}

src/lib/converter/types.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
createSignature,
3838
} from "./factories/signature";
3939
import { convertSymbol } from "./symbols";
40+
import { isObjectType } from "./utils/nodes";
4041
import { removeUndefined } from "./utils/reflections";
4142

4243
export interface TypeConverter<
@@ -1035,10 +1036,6 @@ function requestBugReport(context: Context, nodeOrType: ts.Node | ts.Type) {
10351036
}
10361037
}
10371038

1038-
function isObjectType(type: ts.Type): type is ts.ObjectType {
1039-
return typeof (type as any).objectFlags === "number";
1040-
}
1041-
10421039
function resolveReference(type: ts.Type) {
10431040
if (isObjectType(type) && type.objectFlags & ts.ObjectFlags.Reference) {
10441041
return (type as ts.TypeReference).target;

src/lib/converter/utils/nodes.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,7 @@ export function getHeritageTypes(
3030
return true;
3131
});
3232
}
33+
34+
export function isObjectType(type: ts.Type): type is ts.ObjectType {
35+
return typeof (type as any).objectFlags === "number";
36+
}

src/test/converter2.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ function runTest(
3030
join(base, `${entry}.tsx`),
3131
join(base, `${entry}.js`),
3232
join(base, entry, "index.ts"),
33+
join(base, entry, "index.js"),
3334
].find(existsSync);
3435

3536
ok(entryPoint, `No entry point found for ${entry}`);

src/test/converter2/issues/gh1896.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
/**
1010
* Before tag
11-
* @typedef {{(one: number, two: number) => number}} Type2
11+
* @typedef {{(one: number, two: number): number}} Type2
1212
*
1313
* Some type 2.
1414
*/
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
export { other } from "./other";
2+
3+
/** @typedef {import("./other").Foo} Foo */
4+
/** @typedef {import("./other").Foo} RenamedFoo */
5+
6+
/**
7+
* @typedef {import("./other").Generic<T>} Generic
8+
* @template T
9+
*/
10+
11+
/**
12+
* @typedef {import("./other").Generic<U>} RenamedGeneric
13+
* @template {string} U
14+
*/
15+
16+
/**
17+
* @typedef {import("./other").Generic<string>} NonGeneric
18+
*/
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/** @typedef {string} Foo */
2+
3+
/**
4+
* @typedef {T extends `a${infer F}` ? F : never} Generic
5+
* @template {string} T
6+
*/
7+
8+
export const other = 123;

src/test/converter2/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"compilerOptions": {
33
"strict": true,
44
"module": "CommonJS",
5-
"allowJs": true,
5+
"checkJs": true,
66
"outDir": "dist",
77
"target": "ESNext",
88

src/test/issueTests.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
UnionType,
1717
LiteralType,
1818
IntrinsicType,
19+
ReferenceReflection,
1920
} from "../lib/models";
2021
import type { InlineTagDisplayPart } from "../lib/models/comments/comment";
2122
import { getConverter2App } from "./programs";
@@ -766,4 +767,17 @@ export const issueTests: {
766767
equal(MultipleSimpleCtors.type.declaration.signatures?.length, 2);
767768
equal(AnotherCtor.type.declaration.signatures?.length, 1);
768769
},
770+
771+
gh2044(project) {
772+
for (const [name, ref] of [
773+
["Foo", false],
774+
["RenamedFoo", true],
775+
["Generic", false],
776+
["RenamedGeneric", true],
777+
["NonGeneric", false],
778+
] as const) {
779+
const decl = query(project, name);
780+
equal(decl instanceof ReferenceReflection, ref, `${name} = ${ref}`);
781+
}
782+
},
769783
};

0 commit comments

Comments
 (0)