Skip to content

Commit

Permalink
Produce warnings if links cannot be resolved
Browse files Browse the repository at this point in the history
Ref: #2808
  • Loading branch information
Gerrit0 committed Dec 14, 2024
1 parent e4f991d commit 9eb1e05
Show file tree
Hide file tree
Showing 27 changed files with 313 additions and 31 deletions.
2 changes: 2 additions & 0 deletions .config/typedoc.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
],
"name": "TypeDoc API",

// Don't document the debug entry point
"entryPoints": ["../src/index.ts"],
"outputs": [
{
"name": "html",
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ title: Changelog

### Bug Fixes

- Possibly Breaking: TypeDoc will no longer render anchors within the page for
deeply nested properties. This only affects links to properties of
properties of types, which did not have a clickable link exposed so are
unlikely to have been linked to. Furthermore, these links were not always
created by TypeDoc, only being created if all parent properties contained
comments, #2808.
- TypeDoc will now warn if a property which does not have a URL within the
rendered document and the parent property/page will be linked to instead,
#2808. These warnings can be disabled with the `validation.rewrittenLink`
option.
- Fix restoration of groups/categories including documents, #2801.
- Fixed missed relative paths within markdown link references in documents.
- Improved handling of incomplete inline code blocks within markdown.
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"exports": {
".": "./dist/index.js",
"./tsdoc.json": "./tsdoc.json",
"./package.json": "./package.json"
"./package.json": "./package.json",
"./debug": "./dist/lib/debug/index.js"
},
"types": "./dist/index.d.ts",
"bin": {
Expand Down
3 changes: 3 additions & 0 deletions scripts/capture_screenshots.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ export async function captureScreenshots(
headless,
theme,
) {
await fs.promises.rm(outputDirectory, { force: true, recursive: true });
await fs.promises.mkdir(outputDirectory, { recursive: true });

const browser = await puppeteer.launch({
args:
platform() === "win32"
Expand Down
3 changes: 2 additions & 1 deletion site/development/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ to. Plugins should assume that they may be loaded multiple times for different
applications, and that a single load of an application class may be used to
convert multiple projects.

Plugins may be either ESM or CommonJS.
Plugins may be either ESM or CommonJS, but TypeDoc ships with ESM, so they should
generally published as ESM to avoid `require(esm)` experimental warnings.

```js
// @ts-check
Expand Down
21 changes: 20 additions & 1 deletion site/options/validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,32 @@ typedoc.json (defaults):
"validation": {
"notExported": true,
"invalidLink": true,
"rewrittenLink": true,
"notDocumented": false,
"unusedMergeModuleWith": true
}
}
```

Specifies validation steps TypeDoc should perform on your generated documentation.
Specifies validation steps TypeDoc should perform on your generated
documentation. Most validation occurs before rendering, but `rewrittenLink` is
done during HTML rendering as links have not been generated before rendering
begins.

- **notExported** - Produce warnings if a type is referenced by the
documentation but the type isn't exported and therefore included in the
documentation.
- **invalidLink** - Produce warnings for `@link` tags which cannot be resolved.
- **rewrittenLink** - Produce warnings for `@link` tags which are resolved,
but whose target does not have a unique URL in the documentation. TypeDoc
will rewrite these links to point to the first parent with a URL.
- **notDocumented** - Produce warnings for reflections which do not have a
documentation comment. This is also controlled by the
[requiredToBeDocumented](#requiredtobedocumented) option.
- **unusedMergeModuleWith** - Produce warnings for
[`@mergeModuleWith`](../tags/mergeModuleWith.md) tags which are not
resolved. This option should generally be disabled if generating JSON which
will be combined with another document later.

## treatWarningsAsErrors

Expand Down
5 changes: 5 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
/**
* @module TypeDoc API
*
* In addition to the members documented here, TypeDoc exports a `typedoc/debug`
* entry point which exports some functions which may be useful during plugin
* development or debugging. Exports from that entry point are **not stable**
* and may change or be removed at any time.
*/
export { Application, type ApplicationEvents } from "./lib/application.js";

Expand Down
25 changes: 17 additions & 8 deletions src/lib/converter/comments/declarationReferenceResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ok } from "assert";
import {
ContainerReflection,
DeclarationReflection,
type DocumentReflection,
type ProjectReflection,
ReferenceReflection,
type Reflection,
Expand Down Expand Up @@ -225,10 +226,18 @@ function resolveSymbolReferencePart(
let high: Reflection[] = [];
let low: Reflection[] = [];

if (
!(refl instanceof ContainerReflection) ||
!refl.childrenIncludingDocuments
) {
let children:
| ReadonlyArray<DocumentReflection | DeclarationReflection>
| undefined;

if (refl instanceof ContainerReflection) {
children = refl.childrenIncludingDocuments;
}
if (!children && refl.isDeclaration() && refl.type?.type === "reflection") {
children = refl.type.declaration.childrenIncludingDocuments;
}

if (!children) {
return { high, low };
}

Expand All @@ -238,12 +247,12 @@ function resolveSymbolReferencePart(
// so that resolution doesn't behave very poorly with projects using JSDoc style resolution.
// Also is more consistent with how TypeScript resolves link tags.
case ".":
high = refl.childrenIncludingDocuments.filter(
high = children.filter(
(r) =>
r.name === path.path &&
(r.kindOf(ReflectionKind.SomeExport) || r.flags.isStatic),
);
low = refl.childrenIncludingDocuments.filter(
low = children.filter(
(r) =>
r.name === path.path &&
(!r.kindOf(ReflectionKind.SomeExport) || !r.flags.isStatic),
Expand All @@ -254,7 +263,7 @@ function resolveSymbolReferencePart(
// enum members, type literal properties
case "#":
high =
refl.children?.filter((r) => {
children?.filter((r) => {
return (
r.name === path.path &&
r.kindOf(ReflectionKind.SomeMember) &&
Expand All @@ -269,7 +278,7 @@ function resolveSymbolReferencePart(
if (
refl.kindOf(ReflectionKind.SomeModule | ReflectionKind.Project)
) {
high = refl.children?.filter((r) => r.name === path.path) || [];
high = children?.filter((r) => r.name === path.path) || [];
}
break;
}
Expand Down
15 changes: 12 additions & 3 deletions src/lib/converter/comments/linkResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
DeclarationReflection,
type InlineTagDisplayPart,
Reflection,
ReflectionKind,
ReflectionSymbolId,
} from "../../models/index.js";
import {
Expand Down Expand Up @@ -131,12 +132,20 @@ function resolveLinkTag(

// Might already know where it should go if useTsLinkResolution is turned on
if (part.target instanceof ReflectionSymbolId) {
const tsTarget = reflection.project.getReflectionFromSymbolId(
const tsTargets = reflection.project.getReflectionsFromSymbolId(
part.target,
);

if (tsTarget) {
target = tsTarget;
if (tsTargets.length) {
// Find the target most likely to have a real url in the generated documentation
target =
tsTargets.find((r) => r.kindOf(ReflectionKind.SomeExport)) ||
tsTargets.find(
(r) =>
r.kindOf(ReflectionKind.SomeMember) &&
r.parent?.kindOf(ReflectionKind.SomeExport),
) ||
tsTargets[0];
pos = end;
defaultDisplayText =
part.tsLinkText ||
Expand Down
25 changes: 25 additions & 0 deletions src/lib/debug/debugReflectionLifetimes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/* eslint-disable no-console */
import type { Application } from "../application.js";
import { ConverterEvents } from "../converter/converter-events.js";
import type { Reflection } from "../models/index.js";

export function debugReflectionLifetimes(app: Application) {
app.converter.on(ConverterEvents.CREATE_PROJECT, logCreate);
app.converter.on(ConverterEvents.CREATE_SIGNATURE, logCreate);
app.converter.on(ConverterEvents.CREATE_TYPE_PARAMETER, logCreate);
app.converter.on(ConverterEvents.CREATE_DECLARATION, logCreate);
app.converter.on(ConverterEvents.CREATE_DOCUMENT, logCreate);
app.converter.on(ConverterEvents.CREATE_PARAMETER, logCreate);

app.converter.on(ConverterEvents.CREATE_PROJECT, (_context, project) => {
const oldRemove = project["_removeReflection"];
project["_removeReflection"] = function (reflection) {
console.log("Remove", reflection.id, reflection.getFullName());
return oldRemove.call(this, reflection);
};
});
}

function logCreate(_context: unknown, refl: Reflection) {
console.log("Create", refl.variant, refl.id, refl.getFullName());
}
88 changes: 88 additions & 0 deletions src/lib/debug/debugRendererUrls.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/* eslint-disable no-console */
import { join } from "node:path";
import type { Application } from "../application.js";
import { Reflection, type SomeReflection } from "../models/index.js";
import type { SerializerComponent } from "../serialization/components.js";
import type { JSONOutput } from "../serialization/index.js";

const serializer: SerializerComponent<SomeReflection> = {
priority: 0,
supports(x) {
return x instanceof Reflection;
},
toObject(item, obj: any) {
obj.url = item.url;
obj.hasOwnDocument = item.hasOwnDocument;
// obj.anchor = item.anchor;
delete obj.sources;
delete obj.groups;
delete obj.categories;
delete obj.readme;
delete obj.content;
delete obj.kind;
delete obj.flags;
delete obj.defaultValue;
delete obj.symbolIdMap;
delete obj.files;
delete obj.packageName;
delete obj.variant;
delete obj.extendedTypes;
delete obj.inheritedFrom;
if (!["reflection", "reference"].includes(obj.type?.type)) {
delete obj.type;
}

if (obj.comment) {
obj.comment.summary = obj.comment.summary.filter(
(part: JSONOutput.CommentDisplayPart) =>
part.kind === "inline-tag",
);
obj.comment.blockTags = obj.comment.blockTags?.filter(
(tag: JSONOutput.CommentTag) => {
tag.content = tag.content.filter(
(part) => part.kind === "inline-tag",
);
return tag.content.length;
},
);

if (
!obj.comment.summary.length &&
!obj.comment.blockTags?.length &&
!obj.comment.modifierTags
) {
delete obj.comment;
}
}

return obj;
},
};

export function debugRendererUrls(
app: Application,
{ json = false, logs = false } = { logs: true },
) {
app.renderer.postRenderAsyncJobs.push(async (evt) => {
if (json) {
app.serializer.addSerializer(serializer);
await app.generateJson(
evt.project,
join(evt.outputDirectory, "url_debug.json"),
);
app.serializer.removeSerializer(serializer);
}

if (logs) {
for (const id in evt.project.reflections) {
const refl = evt.project.reflections[id];
console.log(
refl.id,
refl.getFullName(),
refl.url,
refl.hasOwnDocument,
);
}
}
});
}
2 changes: 2 additions & 0 deletions src/lib/debug/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { debugRendererUrls } from "./debugRendererUrls.js";
export { debugReflectionLifetimes } from "./debugReflectionLifetimes.js";
4 changes: 2 additions & 2 deletions src/lib/internationalization/internationalization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,13 +108,13 @@ export class Internationalization {
* Get the translation of the specified key, replacing placeholders
* with the arguments specified.
*/
translate<T extends keyof typeof translatable>(
translate<T extends keyof TranslatableStrings>(
key: T,
...args: TranslatableStrings[T]
): TranslatedString {
return (
this.allTranslations.get(this.application?.lang ?? "en").get(key) ??
translatable[key]
translatable[key as keyof typeof translatable]
).replace(/\{(\d+)\}/g, (_, index) => {
return args[+index] ?? "(no placeholder)";
}) as TranslatedString;
Expand Down
1 change: 1 addition & 0 deletions src/lib/internationalization/locales/en.cts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export = {
"The following symbols were marked as intentionally not exported, but were either not referenced in the documentation, or were exported:\n\t{0}",
reflection_0_has_unused_mergeModuleWith_tag:
"{0} has a @mergeModuleWith tag which could not be resolved",
reflection_0_links_to_1_with_text_2_but_resolved_to_3: `"{0}" links to "{1}" with text "{2}" which exists but does not have a link in the documentation, will link to "{3}" instead.`,

// conversion plugins
not_all_search_category_boosts_used_0: `Not all categories specified in searchCategoryBoosts were used in the documentation. The unused categories were:\n\t{0}`,
Expand Down
1 change: 1 addition & 0 deletions src/lib/models/reflections/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,7 @@ export class ProjectReflection extends ContainerReflection {

/** @internal */
registerSymbolId(reflection: Reflection, id: ReflectionSymbolId) {
this.removedSymbolIds.delete(id);
this.reflectionIdToSymbolIdMap.set(reflection.id, id);

const previous = this.symbolToReflectionIdMap.get(id);
Expand Down
12 changes: 10 additions & 2 deletions src/lib/models/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -833,7 +833,13 @@ export class ReferenceType extends Type {
const resolved =
resolvePotential.find((refl) => refl.kindOf(kind)) ||
resolvePotential.find((refl) => refl.kindOf(~kind))!;
this._target = resolved.id;

// Do not mark the type as resolved at this point so that if it
// points to a member which is removed (e.g. by typedoc-plugin-zod)
// and then replaced it still ends up pointing at the right reflection.
// We will lock type reference resolution when serializing to JSON.
// this._target = resolved.id;

return resolved;
}

Expand Down Expand Up @@ -1002,6 +1008,8 @@ export class ReferenceType extends Type {
target = this._target;
} else if (this._project?.symbolIdHasBeenRemoved(this._target)) {
target = -1;
} else if (this.reflection) {
target = this.reflection.id;
} else {
target = this._target.toObject(serializer);
}
Expand All @@ -1023,7 +1031,7 @@ export class ReferenceType extends Type {
result.refersToTypeParameter = true;
}

if (typeof this._target !== "number" && this.preferValues) {
if (typeof target !== "number" && this.preferValues) {
result.preferValues = true;
}

Expand Down
Loading

0 comments on commit 9eb1e05

Please sign in to comment.