Skip to content

Commit

Permalink
Add support for @class on variables/functions
Browse files Browse the repository at this point in the history
Resolves #2479
  • Loading branch information
Gerrit0 committed Jan 12, 2024
1 parent af0d1b4 commit 36a4d53
Show file tree
Hide file tree
Showing 12 changed files with 284 additions and 14 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@
## Features

- Added a new `--sitemapBaseUrl` option. When specified, TypeDoc will generate a `sitemap.xml` in your output folder that describes the site, #2480.
- 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.
Note: This should only be used on symbols which actually represent a class, but are not declared as a class for some reason.

## Bug Fixes

- Fixed an issue where a namespace would not be created for merged function-namespaces which are declared as variables, #2478.
- Variable functions which have construct signatures will no longer be converted as functions, ignoring the construct signatures.
- 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.

## v0.25.7 (2024-01-08)
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@
"test:cov": "c8 mocha --config .config/mocha.fast.json",
"doc:c": "node bin/typedoc --tsconfig src/test/converter/tsconfig.json",
"doc:cd": "node --inspect-brk bin/typedoc --tsconfig src/test/converter/tsconfig.json",
"doc:c2": "node bin/typedoc --tsconfig src/test/converter2/tsconfig.json",
"doc:c2d": "node --inspect-brk bin/typedoc --tsconfig src/test/converter2/tsconfig.json",
"doc:c2": "node bin/typedoc --options src/test/converter2 --tsconfig src/test/converter2/tsconfig.json",
"doc:c2d": "node --inspect-brk bin/typedoc --options src/test/converter2 --tsconfig src/test/converter2/tsconfig.json",
"example": "cd example && node ../bin/typedoc",
"test:full": "c8 mocha --config .config/mocha.full.json",
"test:visual": "ts-node ./src/test/capture-screenshots.ts && ./scripts/compare_screenshots.sh",
Expand Down
4 changes: 4 additions & 0 deletions src/lib/converter/comments/discovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ const wantedKinds: Record<ReflectionKind, ts.SyntaxKind[]> = {
[ReflectionKind.Class]: [
ts.SyntaxKind.ClassDeclaration,
ts.SyntaxKind.BindingElement,
// If marked with @class
ts.SyntaxKind.VariableDeclaration,
ts.SyntaxKind.ExportAssignment,
ts.SyntaxKind.FunctionDeclaration,
],
[ReflectionKind.Interface]: [
ts.SyntaxKind.InterfaceDeclaration,
Expand Down
57 changes: 57 additions & 0 deletions src/lib/converter/factories/signature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
IntrinsicType,
ParameterReflection,
PredicateType,
ReferenceType,
Reflection,
ReflectionFlag,
ReflectionKind,
Expand Down Expand Up @@ -43,6 +44,11 @@ export function createSignature(
kind,
context.scope,
);
// This feels awful, but we need some way to tell if callable signatures on classes
// are "static" (e.g. `Foo()`) or not (e.g. `(new Foo())()`)
if (context.shouldBeStatic) {
sigRef.setFlag(ReflectionFlag.Static);
}
const sigRefCtx = context.withScope(sigRef);
if (symbol && declaration) {
context.project.registerSymbolId(
Expand Down Expand Up @@ -132,6 +138,57 @@ export function createSignature(
);
}

/**
* Special cased constructor factory for functions tagged with `@class`
*/
export function createConstructSignatureWithType(
context: Context,
signature: ts.Signature,
classType: Reflection,
) {
assert(context.scope instanceof DeclarationReflection);

const declaration = signature.getDeclaration() as
| ts.SignatureDeclaration
| undefined;

const sigRef = new SignatureReflection(
`new ${context.scope.parent!.name}`,
ReflectionKind.ConstructorSignature,
context.scope,
);
const sigRefCtx = context.withScope(sigRef);

if (declaration) {
sigRef.comment = context.getSignatureComment(declaration);
}

sigRef.typeParameters = convertTypeParameters(
sigRefCtx,
sigRef,
signature.typeParameters,
);

sigRef.type = ReferenceType.createResolvedReference(
context.scope.parent!.name,
classType,
context.project,
);

context.registerReflection(sigRef, undefined);

context.scope.signatures ??= [];
context.scope.signatures.push(sigRef);

context.converter.trigger(
ConverterEvents.CREATE_SIGNATURE,
context,
sigRef,
declaration,
signature,
);
}

function convertParameters(
context: Context,
sigRef: SignatureReflection,
Expand Down
101 changes: 99 additions & 2 deletions src/lib/converter/symbols.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type { Context } from "./context";
import { convertDefaultValue } from "./convert-expression";
import { convertIndexSignature } from "./factories/index-signature";
import {
createConstructSignatureWithType,
createSignature,
createTypeParamReflection,
} from "./factories/signature";
Expand Down Expand Up @@ -72,9 +73,9 @@ const conversionOrder = [
ts.SymbolFlags.BlockScopedVariable,
ts.SymbolFlags.FunctionScopedVariable,
ts.SymbolFlags.ExportValue,
ts.SymbolFlags.Function, // Before NamespaceModule

ts.SymbolFlags.TypeAlias,
ts.SymbolFlags.Function, // Before NamespaceModule
ts.SymbolFlags.Method,
ts.SymbolFlags.Interface,
ts.SymbolFlags.Property,
Expand Down Expand Up @@ -427,6 +428,13 @@ function convertFunctionOrMethod(
(ts.SymbolFlags.Property | ts.SymbolFlags.Method)
);

if (!isMethod) {
const comment = context.getComment(symbol, ReflectionKind.Function);
if (comment?.hasModifier("@class")) {
return convertSymbolAsClass(context, symbol, exportSymbol);
}
}

const declarations =
symbol.getDeclarations()?.filter(ts.isFunctionLike) ?? [];

Expand Down Expand Up @@ -892,7 +900,14 @@ function convertVariable(
return convertVariableAsNamespace(context, symbol, exportSymbol);
}

if (type.getCallSignatures().length) {
if (comment?.hasModifier("@class")) {
return convertSymbolAsClass(context, symbol, exportSymbol);
}

if (
type.getCallSignatures().length &&
!type.getConstructSignatures().length
) {
return convertVariableAsFunction(context, symbol, exportSymbol);
}

Expand Down Expand Up @@ -1075,6 +1090,88 @@ function convertFunctionProperties(
return ts.SymbolFlags.None;
}

function convertSymbolAsClass(
context: Context,
symbol: ts.Symbol,
exportSymbol?: ts.Symbol,
) {
const reflection = context.createDeclarationReflection(
ReflectionKind.Class,
symbol,
exportSymbol,
);
const rc = context.withScope(reflection);

context.finalizeDeclarationReflection(reflection);

if (!symbol.valueDeclaration) {
context.logger.error(
`No value declaration found when converting ${symbol.name} as a class`,
symbol.declarations?.[0],
);
return;
}

const type = context.checker.getTypeOfSymbolAtLocation(
symbol,
symbol.valueDeclaration,
);

rc.shouldBeStatic = true;
convertSymbols(
rc,
// Prototype is implicitly this class, don't document it.
type.getProperties().filter((prop) => prop.name !== "prototype"),
);

for (const sig of type.getCallSignatures()) {
createSignature(rc, ReflectionKind.CallSignature, sig, undefined);
}

rc.shouldBeStatic = false;

const ctors = type.getConstructSignatures();
if (ctors.length) {
const constructMember = rc.createDeclarationReflection(
ReflectionKind.Constructor,
ctors?.[0]?.declaration?.symbol,
void 0,
"constructor",
);

// Modifiers are the same for all constructors
if (ctors.length && ctors[0].declaration) {
setModifiers(symbol, ctors[0].declaration, constructMember);
}

context.finalizeDeclarationReflection(constructMember);

const constructContext = rc.withScope(constructMember);

for (const sig of ctors) {
createConstructSignatureWithType(constructContext, sig, reflection);
}

const instType = ctors[0].getReturnType();
convertSymbols(rc, instType.getProperties());

for (const sig of instType.getCallSignatures()) {
createSignature(rc, ReflectionKind.CallSignature, sig, undefined);
}
} else {
context.logger.warn(
`${reflection.getFriendlyFullName()} is being converted as a class, but does not have any construct signatures`,
symbol.valueDeclaration,
);
}

return (
ts.SymbolFlags.TypeAlias |
ts.SymbolFlags.Interface |
ts.SymbolFlags.Namespace
);
}

function convertAccessor(
context: Context,
symbol: ts.Symbol,
Expand Down
1 change: 1 addition & 0 deletions src/lib/utils/options/tsdoc-defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export const modifierTags = [
...tsdocModifierTags,
"@hidden",
"@ignore",
"@class",
"@enum",
"@event",
"@overload",
Expand Down
63 changes: 62 additions & 1 deletion src/test/behavior.c2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
import { join } from "path";
import { existsSync } from "fs";
import { clearCommentCache } from "../lib/converter/comments";
import { query, querySig } from "./utils";
import { getComment, query, querySig } from "./utils";

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

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

afterEach(() => {
app.options.restore(optionsSnap);
logger.expectNoOtherMessages();
});

it("Handles 'as const' style enums", () => {
Expand Down Expand Up @@ -221,6 +222,63 @@ describe("Behavior Tests", () => {
);
});

it("Should allow the user to mark a variable or function as a class with @class", () => {
const project = convert("classTag");
logger.expectMessage(
`warn: BadClass is being converted as a class, but does not have any construct signatures`,
);

const CallableClass = query(project, "CallableClass");
equal(CallableClass.signatures?.length, 2);
equal(
CallableClass.signatures.map((sig) => sig.type?.toString()),
["number", "string"],
);
equal(
CallableClass.signatures.map((sig) => sig.flags.isStatic),
[true, false],
);
equal(
CallableClass.children?.map((child) => [
child.name,
ReflectionKind.singularString(child.kind),
]),
[
[
"constructor",
ReflectionKind.singularString(ReflectionKind.Constructor),
],
[
"inst",
ReflectionKind.singularString(ReflectionKind.Property),
],
[
"stat",
ReflectionKind.singularString(ReflectionKind.Property),
],
[
"method",
ReflectionKind.singularString(ReflectionKind.Method),
],
],
);

equal(query(project, "CallableClass.stat").flags.isStatic, true);

equal(
["VariableClass", "VariableClass.stat", "VariableClass.inst"].map(
(name) => getComment(project, name),
),
["Variable class", "Stat docs", "Inst docs"],
);

equal(project.children?.map((c) => c.name), [
"BadClass",
"CallableClass",
"VariableClass",
]);
});

it("Handles const type parameters", () => {
const project = convert("constTypeParam");
const getNamesExactly = query(project, "getNamesExactly");
Expand Down Expand Up @@ -831,6 +889,9 @@ describe("Behavior Tests", () => {
logger.expectMessage(
"warn: MultiCommentMultiDeclaration has multiple declarations with a comment. An arbitrary comment will be used.",
);
logger.expectMessage(
"info: The comments for MultiCommentMultiDeclaration are declared at*",
);
});

it("Handles named tuple declarations", () => {
Expand Down
1 change: 0 additions & 1 deletion src/test/converter/comment/comment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import "./comment2";
* @deprecated
* @todo something
*
* @class will be removed
* @type {Data<object>} will also be removed
*/
export class CommentedClass {
Expand Down

0 comments on commit 36a4d53

Please sign in to comment.