From 2cae7913ef294474b6e13aba3977b50f27bb9b12 Mon Sep 17 00:00:00 2001 From: Gerrit Birkeland Date: Sat, 25 Nov 2023 15:34:24 -0700 Subject: [PATCH] Correctly handle transient symbols Resolves #2444 --- CHANGELOG.md | 1 + package.json | 1 + .../models/reflections/ReflectionSymbolId.ts | 22 ++++++++++- src/test/converter2/issues/gh2444.ts | 37 +++++++++++++++++++ src/test/issues.c2.test.ts | 26 ++++++++----- 5 files changed, 76 insertions(+), 11 deletions(-) create mode 100644 src/test/converter2/issues/gh2444.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 580b05317..42a8aa9e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Improved handling of function-modules created with `Object.assign`, #2436. - TypeDoc will no longer warn about duplicate comments with warnings which point to a single comment, #2437 - Fixed an infinite loop when `skipLibCheck` is used to ignore some compiler errors, #2438. +- Correctly handle transient symbols in `@namespace`-created namespaces, #2444. ### Thanks! diff --git a/package.json b/package.json index f0bdf81f2..7ba671476 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "test:cov": "c8 mocha --config .config/mocha.fast.json", "doc:c": "node 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", "test:full": "c8 mocha --config .config/mocha.full.json", "test:visual": "ts-node ./src/test/capture-screenshots.ts && ./scripts/compare_screenshots.sh", "test:visual:accept": "node scripts/accept_visual_regression.js", diff --git a/src/lib/models/reflections/ReflectionSymbolId.ts b/src/lib/models/reflections/ReflectionSymbolId.ts index 638192c72..d3c861871 100644 --- a/src/lib/models/reflections/ReflectionSymbolId.ts +++ b/src/lib/models/reflections/ReflectionSymbolId.ts @@ -3,9 +3,9 @@ import { isAbsolute, join, relative, resolve } from "path"; import ts from "typescript"; import type { JSONOutput, Serializer } from "../../serialization/index"; import { getCommonDirectory, readFile } from "../../utils/fs"; +import { normalizePath } from "../../utils/paths"; import { getQualifiedName } from "../../utils/tsutils"; import { optional, validate } from "../../utils/validation"; -import { normalizePath } from "../../utils/paths"; /** * See {@link ReflectionSymbolId} @@ -14,6 +14,9 @@ export type ReflectionSymbolIdString = string & { readonly __reflectionSymbolId: unique symbol; }; +let transientCount = 0; +const transientIds = new WeakMap(); + /** * This exists so that TypeDoc can store a unique identifier for a `ts.Symbol` without * keeping a reference to the `ts.Symbol` itself. This identifier should be stable across @@ -25,8 +28,16 @@ export class ReflectionSymbolId { /** * Note: This is **not** serialized. It exists for sorting by declaration order, but * should not be needed when deserializing from JSON. + * Will be set to `Infinity` if the ID was deserialized from JSON. */ pos: number; + /** + * Note: This is **not** serialized. It exists to support detection of the differences between + * symbols which share declarations, but are instantiated with different type parameters. + * This will be `NaN` if the symbol reference is not transient. + * Note: This can only be non-NaN if {@link pos} is finite. + */ + transientId: number; constructor(symbol: ts.Symbol, declaration?: ts.Declaration); constructor(json: JSONOutput.ReflectionSymbolId); @@ -45,16 +56,23 @@ export class ReflectionSymbolId { this.qualifiedName = getQualifiedName(symbol, symbol.name); } this.pos = declaration?.pos ?? Infinity; + if (symbol.flags & ts.SymbolFlags.Transient) { + this.transientId = transientIds.get(symbol) ?? ++transientCount; + transientIds.set(symbol, this.transientId); + } else { + this.transientId = NaN; + } } else { this.fileName = symbol.sourceFileName; this.qualifiedName = symbol.qualifiedName; this.pos = Infinity; + this.transientId = NaN; } } getStableKey(): ReflectionSymbolIdString { if (Number.isFinite(this.pos)) { - return `${this.fileName}\0${this.qualifiedName}\0${this.pos}` as ReflectionSymbolIdString; + return `${this.fileName}\0${this.qualifiedName}\0${this.pos}\0${this.transientId}` as ReflectionSymbolIdString; } else { return `${this.fileName}\0${this.qualifiedName}` as ReflectionSymbolIdString; } diff --git a/src/test/converter2/issues/gh2444.ts b/src/test/converter2/issues/gh2444.ts new file mode 100644 index 000000000..916a9bdff --- /dev/null +++ b/src/test/converter2/issues/gh2444.ts @@ -0,0 +1,37 @@ +function Comparable(impl: { compare(a: T, b: T): number }) { + return { + ...impl, + + equal(a: T, b: T) { + return impl.compare(a, b) === 0; + }, + }; +} + +const BooleanComparable = Comparable({ + compare(a, b) { + return +a - +b; + }, +}); + +/** @namespace */ +export const Boolean = { + ...BooleanComparable, + hasInstance(value: unknown): value is boolean { + return typeof value === "boolean"; + }, +}; + +const NumberComparable = Comparable({ + compare(left, right) { + return left === right ? 0 : left < right ? -1 : 1; + }, +}); + +/** @namespace */ +export const Number = { + ...NumberComparable, + hasInstance(value: unknown): value is number { + return typeof value === "number"; + }, +}; diff --git a/src/test/issues.c2.test.ts b/src/test/issues.c2.test.ts index a97127ee4..af957080d 100644 --- a/src/test/issues.c2.test.ts +++ b/src/test/issues.c2.test.ts @@ -3,19 +3,22 @@ import { notDeepStrictEqual as notEqual, ok, } from "assert"; +import { existsSync } from "fs"; +import { join } from "path"; +import { clearCommentCache } from "../lib/converter/comments"; import { + Comment, + CommentTag, DeclarationReflection, + IntrinsicType, + LiteralType, ProjectReflection, QueryType, + ReferenceReflection, ReflectionKind, - SignatureReflection, ReflectionType, - Comment, - CommentTag, + SignatureReflection, UnionType, - LiteralType, - IntrinsicType, - ReferenceReflection, } from "../lib/models"; import type { InlineTagDisplayPart } from "../lib/models/comments/comment"; import { @@ -24,9 +27,6 @@ import { getConverter2Program, } from "./programs"; import { TestLogger } from "./TestLogger"; -import { clearCommentCache } from "../lib/converter/comments"; -import { join } from "path"; -import { existsSync } from "fs"; import { getComment, getLinks, query } from "./utils"; const base = getConverter2Base(); @@ -1222,4 +1222,12 @@ describe("Issue Tests", () => { const bad = query(convert(), "Bad"); equal(bad.kind, ReflectionKind.Interface); }); + + it("Handles transient symbols correctly, #2444", () => { + const project = convert(); + const boolEq = query(project, "Boolean.equal"); + const numEq = query(project, "Number.equal"); + equal(boolEq.signatures![0].parameters![0].type?.toString(), "boolean"); + equal(numEq.signatures![0].parameters![0].type?.toString(), "number"); + }); });