Skip to content

Commit

Permalink
Detect @typedef re-exports
Browse files Browse the repository at this point in the history
See #2044
  • Loading branch information
Gerrit0 committed Sep 5, 2022
1 parent 71fb913 commit 0c1b9c8
Show file tree
Hide file tree
Showing 10 changed files with 118 additions and 6 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
@@ -1,5 +1,10 @@
# Unreleased

### Features

- TypeDoc will now treat `@typedef {import("foo").Bar<Z>} Baz` type declarations which forward type parameters to the imported
symbol as re-exports of that symbol, #2044.

## v0.23.14 (2022-09-03)

### Features
Expand Down
65 changes: 65 additions & 0 deletions src/lib/converter/jsdoc.ts
Expand Up @@ -18,6 +18,7 @@ import {
convertParameterNodes,
convertTypeParameterNodes,
} from "./factories/signature";
import { convertSymbol } from "./symbols";

export function convertJsDocAlias(
context: Context,
Expand All @@ -33,6 +34,15 @@ export function convertJsDocAlias(
return;
}

// If the typedef tag is just referring to another type-space symbol, with no type parameters
// or appropriate forwarding type parameters, then we treat it as a re-export instead of creating
// a type alias with an import type.
const aliasedSymbol = getTypedefReExportTarget(context, declaration);
if (aliasedSymbol) {
convertSymbol(context, aliasedSymbol, exportSymbol ?? symbol);
return;
}

const reflection = context.createDeclarationReflection(
ReflectionKind.TypeAlias,
symbol,
Expand Down Expand Up @@ -165,3 +175,58 @@ function convertTemplateParameterNodes(
const params = (nodes ?? []).flatMap((tag) => tag.typeParameters);
return convertTypeParameterNodes(context, params);
}

function getTypedefReExportTarget(
context: Context,
declaration: ts.JSDocTypedefTag | ts.JSDocEnumTag
): ts.Symbol | undefined {
const typeExpression = declaration.typeExpression;
if (
!ts.isJSDocTypedefTag(declaration) ||
!typeExpression ||
ts.isJSDocTypeLiteral(typeExpression) ||
!ts.isImportTypeNode(typeExpression.type) ||
!typeExpression.type.qualifier ||
!ts.isIdentifier(typeExpression.type.qualifier)
) {
return;
}

const targetSymbol = context.expectSymbolAtLocation(
typeExpression.type.qualifier
);
const decl = targetSymbol.declarations?.[0];

if (
!decl ||
!(
ts.isTypeAliasDeclaration(decl) ||
ts.isInterfaceDeclaration(decl) ||
ts.isJSDocTypedefTag(decl) ||
ts.isJSDocCallbackTag(decl)
)
) {
return;
}

const targetParams = ts.getEffectiveTypeParameterDeclarations(decl);
const localParams = ts.getEffectiveTypeParameterDeclarations(declaration);
const localArgs = typeExpression.type.typeArguments || [];

// If we have type parameters, ensure they are forwarding parameters with no transformations.
// This doesn't check constraints since they aren't checked in JSDoc types.
if (
targetParams.length !== localParams.length ||
localArgs.some(
(arg, i) =>
!ts.isTypeReferenceNode(arg) ||
!ts.isIdentifier(arg.typeName) ||
arg.typeArguments ||
localParams[i]?.name.text !== arg.typeName.text
)
) {
return;
}

return targetSymbol;
}
5 changes: 1 addition & 4 deletions src/lib/converter/types.ts
Expand Up @@ -37,6 +37,7 @@ import {
createSignature,
} from "./factories/signature";
import { convertSymbol } from "./symbols";
import { isObjectType } from "./utils/nodes";
import { removeUndefined } from "./utils/reflections";

export interface TypeConverter<
Expand Down Expand Up @@ -1035,10 +1036,6 @@ function requestBugReport(context: Context, nodeOrType: ts.Node | ts.Type) {
}
}

function isObjectType(type: ts.Type): type is ts.ObjectType {
return typeof (type as any).objectFlags === "number";
}

function resolveReference(type: ts.Type) {
if (isObjectType(type) && type.objectFlags & ts.ObjectFlags.Reference) {
return (type as ts.TypeReference).target;
Expand Down
4 changes: 4 additions & 0 deletions src/lib/converter/utils/nodes.ts
Expand Up @@ -30,3 +30,7 @@ export function getHeritageTypes(
return true;
});
}

export function isObjectType(type: ts.Type): type is ts.ObjectType {
return typeof (type as any).objectFlags === "number";
}
1 change: 1 addition & 0 deletions src/test/converter2.test.ts
Expand Up @@ -30,6 +30,7 @@ function runTest(
join(base, `${entry}.tsx`),
join(base, `${entry}.js`),
join(base, entry, "index.ts"),
join(base, entry, "index.js"),
].find(existsSync);

ok(entryPoint, `No entry point found for ${entry}`);
Expand Down
2 changes: 1 addition & 1 deletion src/test/converter2/issues/gh1896.js
Expand Up @@ -8,7 +8,7 @@

/**
* Before tag
* @typedef {{(one: number, two: number) => number}} Type2
* @typedef {{(one: number, two: number): number}} Type2
*
* Some type 2.
*/
Expand Down
18 changes: 18 additions & 0 deletions src/test/converter2/issues/gh2044/index.js
@@ -0,0 +1,18 @@
export { other } from "./other";

/** @typedef {import("./other").Foo} Foo */
/** @typedef {import("./other").Foo} RenamedFoo */

/**
* @typedef {import("./other").Generic<T>} Generic
* @template T
*/

/**
* @typedef {import("./other").Generic<U>} RenamedGeneric
* @template {string} U
*/

/**
* @typedef {import("./other").Generic<string>} NonGeneric
*/
8 changes: 8 additions & 0 deletions src/test/converter2/issues/gh2044/other.js
@@ -0,0 +1,8 @@
/** @typedef {string} Foo */

/**
* @typedef {T extends `a${infer F}` ? F : never} Generic
* @template {string} T
*/

export const other = 123;
2 changes: 1 addition & 1 deletion src/test/converter2/tsconfig.json
Expand Up @@ -2,7 +2,7 @@
"compilerOptions": {
"strict": true,
"module": "CommonJS",
"allowJs": true,
"checkJs": true,
"outDir": "dist",
"target": "ESNext",

Expand Down
14 changes: 14 additions & 0 deletions src/test/issueTests.ts
Expand Up @@ -16,6 +16,7 @@ import {
UnionType,
LiteralType,
IntrinsicType,
ReferenceReflection,
} from "../lib/models";
import type { InlineTagDisplayPart } from "../lib/models/comments/comment";
import { getConverter2App } from "./programs";
Expand Down Expand Up @@ -766,4 +767,17 @@ export const issueTests: {
equal(MultipleSimpleCtors.type.declaration.signatures?.length, 2);
equal(AnotherCtor.type.declaration.signatures?.length, 1);
},

gh2044(project) {
for (const [name, ref] of [
["Foo", false],
["RenamedFoo", true],
["Generic", false],
["RenamedGeneric", true],
["NonGeneric", false],
] as const) {
const decl = query(project, name);
equal(decl instanceof ReferenceReflection, ref, `${name} = ${ref}`);
}
},
};

0 comments on commit 0c1b9c8

Please sign in to comment.