Skip to content

Commit 36a4d53

Browse files
committed
Add support for @class on variables/functions
Resolves #2479
1 parent af0d1b4 commit 36a4d53

File tree

12 files changed

+284
-14
lines changed

12 files changed

+284
-14
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@
33
## Features
44

55
- Added a new `--sitemapBaseUrl` option. When specified, TypeDoc will generate a `sitemap.xml` in your output folder that describes the site, #2480.
6+
- Added support for the `@class` tag. When added to a comment on a variable or function, TypeDoc will convert the member as a class, #2479.
7+
Note: This should only be used on symbols which actually represent a class, but are not declared as a class for some reason.
68

79
## Bug Fixes
810

911
- Fixed an issue where a namespace would not be created for merged function-namespaces which are declared as variables, #2478.
12+
- Variable functions which have construct signatures will no longer be converted as functions, ignoring the construct signatures.
1013
- Fixed an issue where, if the index section was collapsed when loading the page, all content within it would be hidden until expanded, and a member visibility checkbox was changed.
1114

1215
## v0.25.7 (2024-01-08)

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,8 @@
6565
"test:cov": "c8 mocha --config .config/mocha.fast.json",
6666
"doc:c": "node bin/typedoc --tsconfig src/test/converter/tsconfig.json",
6767
"doc:cd": "node --inspect-brk bin/typedoc --tsconfig src/test/converter/tsconfig.json",
68-
"doc:c2": "node bin/typedoc --tsconfig src/test/converter2/tsconfig.json",
69-
"doc:c2d": "node --inspect-brk bin/typedoc --tsconfig src/test/converter2/tsconfig.json",
68+
"doc:c2": "node bin/typedoc --options src/test/converter2 --tsconfig src/test/converter2/tsconfig.json",
69+
"doc:c2d": "node --inspect-brk bin/typedoc --options src/test/converter2 --tsconfig src/test/converter2/tsconfig.json",
7070
"example": "cd example && node ../bin/typedoc",
7171
"test:full": "c8 mocha --config .config/mocha.full.json",
7272
"test:visual": "ts-node ./src/test/capture-screenshots.ts && ./scripts/compare_screenshots.sh",

src/lib/converter/comments/discovery.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ const wantedKinds: Record<ReflectionKind, ts.SyntaxKind[]> = {
6565
[ReflectionKind.Class]: [
6666
ts.SyntaxKind.ClassDeclaration,
6767
ts.SyntaxKind.BindingElement,
68+
// If marked with @class
69+
ts.SyntaxKind.VariableDeclaration,
70+
ts.SyntaxKind.ExportAssignment,
71+
ts.SyntaxKind.FunctionDeclaration,
6872
],
6973
[ReflectionKind.Interface]: [
7074
ts.SyntaxKind.InterfaceDeclaration,

src/lib/converter/factories/signature.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
IntrinsicType,
77
ParameterReflection,
88
PredicateType,
9+
ReferenceType,
910
Reflection,
1011
ReflectionFlag,
1112
ReflectionKind,
@@ -43,6 +44,11 @@ export function createSignature(
4344
kind,
4445
context.scope,
4546
);
47+
// This feels awful, but we need some way to tell if callable signatures on classes
48+
// are "static" (e.g. `Foo()`) or not (e.g. `(new Foo())()`)
49+
if (context.shouldBeStatic) {
50+
sigRef.setFlag(ReflectionFlag.Static);
51+
}
4652
const sigRefCtx = context.withScope(sigRef);
4753
if (symbol && declaration) {
4854
context.project.registerSymbolId(
@@ -132,6 +138,57 @@ export function createSignature(
132138
);
133139
}
134140

141+
/**
142+
* Special cased constructor factory for functions tagged with `@class`
143+
*/
144+
export function createConstructSignatureWithType(
145+
context: Context,
146+
signature: ts.Signature,
147+
classType: Reflection,
148+
) {
149+
assert(context.scope instanceof DeclarationReflection);
150+
151+
const declaration = signature.getDeclaration() as
152+
| ts.SignatureDeclaration
153+
| undefined;
154+
155+
const sigRef = new SignatureReflection(
156+
`new ${context.scope.parent!.name}`,
157+
ReflectionKind.ConstructorSignature,
158+
context.scope,
159+
);
160+
const sigRefCtx = context.withScope(sigRef);
161+
162+
if (declaration) {
163+
sigRef.comment = context.getSignatureComment(declaration);
164+
}
165+
166+
sigRef.typeParameters = convertTypeParameters(
167+
sigRefCtx,
168+
sigRef,
169+
signature.typeParameters,
170+
);
171+
172+
sigRef.type = ReferenceType.createResolvedReference(
173+
context.scope.parent!.name,
174+
classType,
175+
context.project,
176+
);
177+
178+
context.registerReflection(sigRef, undefined);
179+
180+
context.scope.signatures ??= [];
181+
context.scope.signatures.push(sigRef);
182+
183+
context.converter.trigger(
184+
ConverterEvents.CREATE_SIGNATURE,
185+
context,
186+
sigRef,
187+
declaration,
188+
signature,
189+
);
190+
}
191+
135192
function convertParameters(
136193
context: Context,
137194
sigRef: SignatureReflection,

src/lib/converter/symbols.ts

Lines changed: 99 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import type { Context } from "./context";
2020
import { convertDefaultValue } from "./convert-expression";
2121
import { convertIndexSignature } from "./factories/index-signature";
2222
import {
23+
createConstructSignatureWithType,
2324
createSignature,
2425
createTypeParamReflection,
2526
} from "./factories/signature";
@@ -72,9 +73,9 @@ const conversionOrder = [
7273
ts.SymbolFlags.BlockScopedVariable,
7374
ts.SymbolFlags.FunctionScopedVariable,
7475
ts.SymbolFlags.ExportValue,
76+
ts.SymbolFlags.Function, // Before NamespaceModule
7577

7678
ts.SymbolFlags.TypeAlias,
77-
ts.SymbolFlags.Function, // Before NamespaceModule
7879
ts.SymbolFlags.Method,
7980
ts.SymbolFlags.Interface,
8081
ts.SymbolFlags.Property,
@@ -427,6 +428,13 @@ function convertFunctionOrMethod(
427428
(ts.SymbolFlags.Property | ts.SymbolFlags.Method)
428429
);
429430

431+
if (!isMethod) {
432+
const comment = context.getComment(symbol, ReflectionKind.Function);
433+
if (comment?.hasModifier("@class")) {
434+
return convertSymbolAsClass(context, symbol, exportSymbol);
435+
}
436+
}
437+
430438
const declarations =
431439
symbol.getDeclarations()?.filter(ts.isFunctionLike) ?? [];
432440

@@ -892,7 +900,14 @@ function convertVariable(
892900
return convertVariableAsNamespace(context, symbol, exportSymbol);
893901
}
894902

895-
if (type.getCallSignatures().length) {
903+
if (comment?.hasModifier("@class")) {
904+
return convertSymbolAsClass(context, symbol, exportSymbol);
905+
}
906+
907+
if (
908+
type.getCallSignatures().length &&
909+
!type.getConstructSignatures().length
910+
) {
896911
return convertVariableAsFunction(context, symbol, exportSymbol);
897912
}
898913

@@ -1075,6 +1090,88 @@ function convertFunctionProperties(
10751090
return ts.SymbolFlags.None;
10761091
}
10771092

1093+
function convertSymbolAsClass(
1094+
context: Context,
1095+
symbol: ts.Symbol,
1096+
exportSymbol?: ts.Symbol,
1097+
) {
1098+
const reflection = context.createDeclarationReflection(
1099+
ReflectionKind.Class,
1100+
symbol,
1101+
exportSymbol,
1102+
);
1103+
const rc = context.withScope(reflection);
1104+
1105+
context.finalizeDeclarationReflection(reflection);
1106+
1107+
if (!symbol.valueDeclaration) {
1108+
context.logger.error(
1109+
`No value declaration found when converting ${symbol.name} as a class`,
1110+
symbol.declarations?.[0],
1111+
);
1112+
return;
1113+
}
1114+
1115+
const type = context.checker.getTypeOfSymbolAtLocation(
1116+
symbol,
1117+
symbol.valueDeclaration,
1118+
);
1119+
1120+
rc.shouldBeStatic = true;
1121+
convertSymbols(
1122+
rc,
1123+
// Prototype is implicitly this class, don't document it.
1124+
type.getProperties().filter((prop) => prop.name !== "prototype"),
1125+
);
1126+
1127+
for (const sig of type.getCallSignatures()) {
1128+
createSignature(rc, ReflectionKind.CallSignature, sig, undefined);
1129+
}
1130+
1131+
rc.shouldBeStatic = false;
1132+
1133+
const ctors = type.getConstructSignatures();
1134+
if (ctors.length) {
1135+
const constructMember = rc.createDeclarationReflection(
1136+
ReflectionKind.Constructor,
1137+
ctors?.[0]?.declaration?.symbol,
1138+
void 0,
1139+
"constructor",
1140+
);
1141+
1142+
// Modifiers are the same for all constructors
1143+
if (ctors.length && ctors[0].declaration) {
1144+
setModifiers(symbol, ctors[0].declaration, constructMember);
1145+
}
1146+
1147+
context.finalizeDeclarationReflection(constructMember);
1148+
1149+
const constructContext = rc.withScope(constructMember);
1150+
1151+
for (const sig of ctors) {
1152+
createConstructSignatureWithType(constructContext, sig, reflection);
1153+
}
1154+
1155+
const instType = ctors[0].getReturnType();
1156+
convertSymbols(rc, instType.getProperties());
1157+
1158+
for (const sig of instType.getCallSignatures()) {
1159+
createSignature(rc, ReflectionKind.CallSignature, sig, undefined);
1160+
}
1161+
} else {
1162+
context.logger.warn(
1163+
`${reflection.getFriendlyFullName()} is being converted as a class, but does not have any construct signatures`,
1164+
symbol.valueDeclaration,
1165+
);
1166+
}
1167+
1168+
return (
1169+
ts.SymbolFlags.TypeAlias |
1170+
ts.SymbolFlags.Interface |
1171+
ts.SymbolFlags.Namespace
1172+
);
1173+
}
1174+
10781175
function convertAccessor(
10791176
context: Context,
10801177
symbol: ts.Symbol,

src/lib/utils/options/tsdoc-defaults.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export const modifierTags = [
5151
...tsdocModifierTags,
5252
"@hidden",
5353
"@ignore",
54+
"@class",
5455
"@enum",
5556
"@event",
5657
"@overload",

src/test/behavior.c2.test.ts

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
import { join } from "path";
2020
import { existsSync } from "fs";
2121
import { clearCommentCache } from "../lib/converter/comments";
22-
import { query, querySig } from "./utils";
22+
import { getComment, query, querySig } from "./utils";
2323

2424
type NameTree = { [name: string]: NameTree };
2525

@@ -106,6 +106,7 @@ describe("Behavior Tests", () => {
106106

107107
afterEach(() => {
108108
app.options.restore(optionsSnap);
109+
logger.expectNoOtherMessages();
109110
});
110111

111112
it("Handles 'as const' style enums", () => {
@@ -221,6 +222,63 @@ describe("Behavior Tests", () => {
221222
);
222223
});
223224

225+
it("Should allow the user to mark a variable or function as a class with @class", () => {
226+
const project = convert("classTag");
227+
logger.expectMessage(
228+
`warn: BadClass is being converted as a class, but does not have any construct signatures`,
229+
);
230+
231+
const CallableClass = query(project, "CallableClass");
232+
equal(CallableClass.signatures?.length, 2);
233+
equal(
234+
CallableClass.signatures.map((sig) => sig.type?.toString()),
235+
["number", "string"],
236+
);
237+
equal(
238+
CallableClass.signatures.map((sig) => sig.flags.isStatic),
239+
[true, false],
240+
);
241+
equal(
242+
CallableClass.children?.map((child) => [
243+
child.name,
244+
ReflectionKind.singularString(child.kind),
245+
]),
246+
[
247+
[
248+
"constructor",
249+
ReflectionKind.singularString(ReflectionKind.Constructor),
250+
],
251+
[
252+
"inst",
253+
ReflectionKind.singularString(ReflectionKind.Property),
254+
],
255+
[
256+
"stat",
257+
ReflectionKind.singularString(ReflectionKind.Property),
258+
],
259+
[
260+
"method",
261+
ReflectionKind.singularString(ReflectionKind.Method),
262+
],
263+
],
264+
);
265+
266+
equal(query(project, "CallableClass.stat").flags.isStatic, true);
267+
268+
equal(
269+
["VariableClass", "VariableClass.stat", "VariableClass.inst"].map(
270+
(name) => getComment(project, name),
271+
),
272+
["Variable class", "Stat docs", "Inst docs"],
273+
);
274+
275+
equal(project.children?.map((c) => c.name), [
276+
"BadClass",
277+
"CallableClass",
278+
"VariableClass",
279+
]);
280+
});
281+
224282
it("Handles const type parameters", () => {
225283
const project = convert("constTypeParam");
226284
const getNamesExactly = query(project, "getNamesExactly");
@@ -831,6 +889,9 @@ describe("Behavior Tests", () => {
831889
logger.expectMessage(
832890
"warn: MultiCommentMultiDeclaration has multiple declarations with a comment. An arbitrary comment will be used.",
833891
);
892+
logger.expectMessage(
893+
"info: The comments for MultiCommentMultiDeclaration are declared at*",
894+
);
834895
});
835896

836897
it("Handles named tuple declarations", () => {

src/test/converter/comment/comment.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ import "./comment2";
2727
* @deprecated
2828
* @todo something
2929
*
30-
* @class will be removed
3130
* @type {Data<object>} will also be removed
3231
*/
3332
export class CommentedClass {

0 commit comments

Comments
 (0)