Skip to content

Commit 4dfefe8

Browse files
committed
Finish support for declaration references, still needs tests
Resolves #1949 Resolves #1629 Resolves #488 Resolves #262
1 parent e7afeff commit 4dfefe8

File tree

5 files changed

+215
-33
lines changed

5 files changed

+215
-33
lines changed

.eslintrc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@
3737
"rules": {
3838
"@typescript-eslint/no-floating-promises": 1,
3939

40+
// This one is just annoying since it complains at incomplete code
41+
"no-empty": 0,
42+
4043
// This rule is factually incorrect. Interfaces which extend some type alias can be used to introduce
4144
// new type names. This is useful particularly when dealing with mixins.
4245
"@typescript-eslint/no-empty-interface": 0,

CHANGELOG.md

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,11 @@
11
# Unreleased
22

3-
### Incomplete
4-
5-
These TODOs will be resolved before a full release. ([GitHub project](https://github.com/TypeStrong/typedoc/projects/11))
6-
7-
- Full support for declaration references, #262, #488, #1326, #1845.
8-
- (Maybe) `@copyDoc` tag to copy parts of documentation from other entries.
9-
- (Maybe) `--commentDiscovery` option to delegate discovery to TypeScript.
10-
113
### Breaking Changes
124

135
- Node 12 is no longer officially supported as it has gone end of life as of 2022-04-30. It might still work, but may stop working at any time.
146
- Dropped support for TypeScript before 4.6.
7+
- `{@link}` tags in comments will now be resolved as declaration references similar to TSDoc's declaration references.
8+
For most cases, this will just work. See [the documentation](https://typedoc.org/guides/link-resolution/) for details on how link resolution works.
159
- TypeDoc will now produce warnings for bracketed links (`[[ target ]]`). Use `{@link target}` instead. The `{@link}` syntax will be recognized by [TypeScript 4.3](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-3.html#editor-support-for-link-tags) and later and used to provide better intellisense. TypeDoc version 0.24.0 will remove support for `[[ target ]]` style links.
1610
- `extends` in typedoc.json is now resolved using NodeJS module resolution, so a local path must begin with `./`.
1711
- In the JSON output for `DeclarationReflection`s, `getSignature` is no longer a one-tuple.

src/lib/converter/comments/declarationReference.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,14 @@ export interface DeclarationReference {
3030
symbolReference?: SymbolReference;
3131
}
3232

33+
export interface Meaning {
34+
keyword?: MeaningKeyword;
35+
index?: number;
36+
}
37+
3338
export interface SymbolReference {
3439
path?: ComponentPath[];
35-
meaning?: { keyword?: MeaningKeyword; index?: number };
40+
meaning?: Meaning;
3641
}
3742

3843
export interface ComponentPath {

src/lib/converter/plugins/LinkResolverPlugin.ts

Lines changed: 190 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,20 @@ import { ConverterEvents } from "../converter-events";
55
import {
66
CommentDisplayPart,
77
ContainerReflection,
8+
DeclarationReflection,
89
InlineTagDisplayPart,
910
ReflectionKind,
1011
} from "../../models";
1112
import {
1213
ComponentPath,
1314
DeclarationReference,
15+
Meaning,
16+
MeaningKeyword,
1417
parseDeclarationReference,
1518
} from "../comments/declarationReference";
1619
import ts = require("typescript");
20+
import { ok } from "assert";
21+
import { BindOption, filterMap, ValidationOptions } from "../../utils";
1722

1823
const urlPrefix = /^(http|ftp)s?:\/\//;
1924
const brackets = /\[\[([^\]]+)\]\]/g;
@@ -22,6 +27,9 @@ const brackets = /\[\[([^\]]+)\]\]/g;
2227
*/
2328
@Component({ name: "link-resolver" })
2429
export class LinkResolverPlugin extends ConverterComponent {
30+
@BindOption("validation")
31+
validation!: ValidationOptions;
32+
2533
/**
2634
* Create a new LinkResolverPlugin instance.
2735
*/
@@ -163,7 +171,11 @@ export class LinkResolverPlugin extends ConverterComponent {
163171
part.tag === "@linkcode" ||
164172
part.tag === "@linkplain"
165173
) {
166-
return resolveLinkTag(reflection, part);
174+
return resolveLinkTag(reflection, part, (msg: string) => {
175+
if (this.validation.invalidLink) {
176+
this.application.logger.warn(msg);
177+
}
178+
});
167179
}
168180
}
169181

@@ -199,7 +211,11 @@ export class LinkResolverPlugin extends ConverterComponent {
199211
}
200212
}
201213

202-
function resolveLinkTag(reflection: Reflection, part: InlineTagDisplayPart) {
214+
function resolveLinkTag(
215+
reflection: Reflection,
216+
part: InlineTagDisplayPart,
217+
warn: (message: string) => void
218+
) {
203219
let pos = 0;
204220
const end = part.text.length;
205221

@@ -219,8 +235,15 @@ function resolveLinkTag(reflection: Reflection, part: InlineTagDisplayPart) {
219235
}
220236

221237
// If resolution via a declaration reference failed, revert to the legacy "split and check"
222-
// method... should probably warn here.
223-
if (!target) return legacyResolveLinkTag(reflection, part);
238+
// method... this should go away in 0.24, once people have had a chance to migrate any failing links.
239+
if (!target) {
240+
warn(
241+
`Failed to resolve {@link ${
242+
part.text
243+
}} in ${reflection.getFriendlyFullName()} with declaration references. This link will break in v0.24.`
244+
);
245+
return legacyResolveLinkTag(reflection, part);
246+
}
224247

225248
// Remaining text after an optional pipe is the link text, so advance
226249
// until that's consumed.
@@ -261,58 +284,201 @@ function resolveDeclarationReference(
261284
reflection: Reflection,
262285
ref: DeclarationReference
263286
): Reflection | undefined {
264-
let refl: Reflection | undefined = reflection;
287+
let high: Reflection[] = [];
288+
let low: Reflection[] = [];
265289

266290
if (ref.moduleSource) {
267-
refl = refl.project.children?.find((c) => c.name === ref.moduleSource);
291+
high =
292+
reflection.project.children?.filter(
293+
(c) =>
294+
c.kindOf(ReflectionKind.SomeModule) &&
295+
c.name === ref.moduleSource
296+
) || [];
268297
} else if (ref.resolutionStart === "global") {
269-
refl = refl.project;
270-
}
298+
high.push(reflection.project);
299+
} else {
300+
ok(ref.resolutionStart === "local");
301+
// TypeScript's behavior is to first try to resolve links via variable scope, and then
302+
// if the link still hasn't been found, check either siblings (if comment belongs to a member)
303+
// or otherwise children.
304+
let refl: Reflection | undefined = reflection;
305+
if (refl.kindOf(ReflectionKind.ExportContainer)) {
306+
high.push(refl);
307+
}
308+
while (refl.parent) {
309+
refl = refl.parent;
310+
if (refl.kindOf(ReflectionKind.ExportContainer)) {
311+
high.push(refl);
312+
}
313+
}
271314

272-
if (!refl) return;
315+
if (reflection.kindOf(ReflectionKind.SomeMember)) {
316+
high.push(reflection.parent!);
317+
} else if (
318+
reflection.kindOf(ReflectionKind.SomeSignature) &&
319+
reflection.parent!.kindOf(ReflectionKind.SomeMember)
320+
) {
321+
high.push(reflection.parent!.parent!);
322+
} else if (high[0] !== reflection) {
323+
high.push(reflection);
324+
}
325+
}
273326

274327
if (ref.symbolReference) {
275-
let targets = [refl];
276328
for (const part of ref.symbolReference.path || []) {
277-
targets = targets.flatMap((refl) =>
278-
resolveSymbolReferencePart(refl, part)
279-
);
329+
const high2 = high;
330+
high = [];
331+
const low2 = low;
332+
low = [];
333+
334+
for (const refl of high2) {
335+
const resolved = resolveSymbolReferencePart(refl, part);
336+
high.push(...resolved.high);
337+
low.push(...resolved.low);
338+
}
339+
340+
for (const refl of low2) {
341+
const resolved = resolveSymbolReferencePart(refl, part);
342+
low.push(...resolved.high);
343+
low.push(...resolved.low);
344+
}
280345
}
281346

282-
// TODO: meaning
283-
return targets[0];
347+
if (ref.symbolReference.meaning) {
348+
high = filterMapByMeaning(high, ref.symbolReference.meaning);
349+
low = filterMapByMeaning(low, ref.symbolReference.meaning);
350+
}
284351
}
285352

286-
return refl;
353+
return high[0] || low[0];
354+
}
355+
356+
function filterMapByMeaning(
357+
reflections: Reflection[],
358+
meaning: Meaning
359+
): Reflection[] {
360+
return filterMap(reflections, (refl): Reflection | undefined => {
361+
const kwResolved = resolveKeyword(refl, meaning.keyword) || [];
362+
return kwResolved[meaning.index || 0];
363+
});
364+
}
365+
366+
function resolveKeyword(
367+
refl: Reflection,
368+
kw: MeaningKeyword | undefined
369+
): Reflection[] | undefined {
370+
switch (kw) {
371+
case undefined:
372+
return [refl];
373+
case "class":
374+
if (refl.kindOf(ReflectionKind.Class)) return [refl];
375+
break;
376+
case "interface":
377+
if (refl.kindOf(ReflectionKind.Interface)) return [refl];
378+
break;
379+
case "type":
380+
if (refl.kindOf(ReflectionKind.SomeType)) return [refl];
381+
break;
382+
case "enum":
383+
if (refl.kindOf(ReflectionKind.Enum)) return [refl];
384+
break;
385+
case "namespace":
386+
if (refl.kindOf(ReflectionKind.SomeModule)) return [refl];
387+
break;
388+
case "function":
389+
if (refl.kindOf(ReflectionKind.FunctionOrMethod)) {
390+
return (refl as DeclarationReflection).signatures;
391+
}
392+
break;
393+
case "var":
394+
if (refl.kindOf(ReflectionKind.Variable)) return [refl];
395+
break;
396+
397+
case "new":
398+
case "constructor":
399+
if (refl.kindOf(ReflectionKind.Class)) {
400+
const ctor = (refl as ContainerReflection).children?.find((c) =>
401+
c.kindOf(ReflectionKind.Constructor)
402+
);
403+
return (ctor as DeclarationReflection)?.signatures;
404+
}
405+
if (refl.kindOf(ReflectionKind.Constructor)) {
406+
return (refl as DeclarationReflection).signatures;
407+
}
408+
break;
409+
410+
case "member":
411+
if (refl.kindOf(ReflectionKind.SomeMember)) return [refl];
412+
break;
413+
case "event":
414+
if (refl.comment?.hasModifier("@event")) return [refl];
415+
break;
416+
case "call":
417+
return (refl as DeclarationReflection).signatures;
418+
419+
case "index":
420+
if ((refl as DeclarationReflection).indexSignature) {
421+
return [(refl as DeclarationReflection).indexSignature!];
422+
}
423+
break;
424+
425+
case "complex":
426+
if (refl.kindOf(ReflectionKind.SomeType)) return [refl];
427+
break;
428+
}
287429
}
288430

289431
function resolveSymbolReferencePart(
290432
refl: Reflection,
291433
path: ComponentPath
292-
): Reflection[] {
293-
if (!(refl instanceof ContainerReflection) || !refl.children) return [];
434+
): { high: Reflection[]; low: Reflection[] } {
435+
let high: Reflection[] = [];
436+
let low: Reflection[] = [];
437+
438+
if (!(refl instanceof ContainerReflection) || !refl.children) {
439+
return { high, low };
440+
}
294441

295442
switch (path.navigation) {
296443
// Grammar says resolve via "exports"... as always, reality is more complicated.
297444
// Check exports first, but also allow this as a general purpose "some child" operator
298445
// so that resolution doesn't behave very poorly with projects using JSDoc style resolution.
299446
// Also is more consistent with how TypeScript resolves link tags.
300447
case ".":
301-
return []; // TODO: Finish me
448+
high = refl.children.filter(
449+
(r) =>
450+
r.name === path.path &&
451+
(r.kindOf(ReflectionKind.SomeExport) || r.flags.isStatic)
452+
);
453+
low = refl.children.filter(
454+
(r) =>
455+
r.name === path.path &&
456+
(!r.kindOf(ReflectionKind.SomeExport) || !r.flags.isStatic)
457+
);
458+
break;
302459

303460
// Resolve via "members", interface children, class instance properties/accessors/methods,
304461
// enum members, type literal properties
305462
case "#":
306-
return refl.children.filter((r) => {
307-
return r.kindOf(ReflectionKind.SomeMember) && !r.flags.isStatic;
463+
high = refl.children.filter((r) => {
464+
return (
465+
r.name === path.path &&
466+
r.kindOf(ReflectionKind.SomeMember) &&
467+
!r.flags.isStatic
468+
);
308469
});
470+
break;
309471

310472
// Resolve via "locals"... treat this as a stricter `.` which only supports traversing
311473
// module/namespace exports since TypeDoc doesn't support documenting locals.
312474
case "~":
313-
if (refl.kindOf(ReflectionKind.SomeModule)) {
314-
return refl.children.filter((r) => r.name === path.path) || [];
475+
if (
476+
refl.kindOf(ReflectionKind.SomeModule | ReflectionKind.Project)
477+
) {
478+
high = refl.children.filter((r) => r.name === path.path);
315479
}
316-
return [];
480+
break;
317481
}
482+
483+
return { high, low };
318484
}

src/lib/models/reflections/kind.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,20 @@ export namespace ReflectionKind {
6666
ReflectionKind.Method |
6767
ReflectionKind.Accessor;
6868

69+
export const SomeExport =
70+
ReflectionKind.Module |
71+
ReflectionKind.Namespace |
72+
ReflectionKind.Enum |
73+
ReflectionKind.Variable |
74+
ReflectionKind.Function |
75+
ReflectionKind.Class |
76+
ReflectionKind.Interface |
77+
ReflectionKind.TypeAlias |
78+
ReflectionKind.Reference;
79+
80+
export const ExportContainer =
81+
ReflectionKind.SomeModule | ReflectionKind.Project;
82+
6983
/** @internal */
7084
export const Inheritable =
7185
ReflectionKind.Accessor |

0 commit comments

Comments
 (0)