Skip to content

Commit ebc3e10

Browse files
authored
build: fix docs-content missing some inherited member description (#24310)
This happened due to: 9973f1d Recently we added logic to exclude resolved external API docs from the Dgeni pipeline, preventing them from being processed by the Dgeni JSDoc processor. This was necessary due to a bug in Dgeni which resulted in runtime errors for unknown JSDoc tags. Some tags from external API docs were unknown because the framework repository uses different sets of annotations/tags. At this point, it seemed reasonable to never add such external API docs to the Dgeni pipeline, while also working around this runtime exception that way. Now after seeing the results, it became clear that we still want to continue processing external API docs as some of our public-facing API docs might end up merging external API document members. In this docs-content built it can be observed that some inherited/merged members no longer have a proper resolved description: angular/material2-docs-content@6b602cf This commit fixes it by adding these external JSDoc tags temporarily as known tags until the Dgeni upstream fix is available. As part of this change we introduced a custom processor to error for unknown JSDoc tags (we want to be strict about this). This will become helpful as Dgeni only warns about unknown tags (and this is non-configurable). Also while being at it the types for `tags` has been improved to avoid the use of `any` everywhere.
1 parent faa3072 commit ebc3e10

File tree

6 files changed

+151
-39
lines changed

6 files changed

+151
-39
lines changed

tools/dgeni/common/decorators.ts

Lines changed: 18 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,20 @@ import {ClassExportDoc} from 'dgeni-packages/typescript/api-doc-types/ClassExpor
33
import {MemberDoc} from 'dgeni-packages/typescript/api-doc-types/MemberDoc';
44
import {PropertyMemberDoc} from 'dgeni-packages/typescript/api-doc-types/PropertyMemberDoc';
55
import {CategorizedClassDoc, DeprecationInfo, HasDecoratorsDoc} from './dgeni-definitions';
6+
import {findJsDocTag, hasJsDocTag} from './tags';
67

7-
export function isMethod(doc: MemberDoc) {
8+
export function isMethod(doc: MemberDoc): boolean {
89
return doc.hasOwnProperty('parameters') && !doc.isGetAccessor && !doc.isSetAccessor;
910
}
1011

11-
export function isGenericTypeParameter(doc: MemberDoc) {
12+
export function isGenericTypeParameter(doc: MemberDoc): boolean {
1213
if (doc.containerDoc instanceof ClassExportDoc) {
13-
return doc.containerDoc.typeParams && `<${doc.name}>` === doc.containerDoc.typeParams;
14+
return !!doc.containerDoc.typeParams && `<${doc.name}>` === doc.containerDoc.typeParams;
1415
}
1516
return false;
1617
}
1718

18-
export function isProperty(doc: MemberDoc) {
19+
export function isProperty(doc: MemberDoc): boolean {
1920
if (
2021
doc instanceof PropertyMemberDoc ||
2122
// The latest Dgeni version no longer treats getters or setters as properties.
@@ -28,30 +29,28 @@ export function isProperty(doc: MemberDoc) {
2829
return false;
2930
}
3031

31-
export function isDirective(doc: ClassExportDoc) {
32+
export function isDirective(doc: ClassExportDoc): boolean {
3233
return hasClassDecorator(doc, 'Component') || hasClassDecorator(doc, 'Directive');
3334
}
3435

35-
export function isService(doc: ClassExportDoc) {
36+
export function isService(doc: ClassExportDoc): boolean {
3637
return hasClassDecorator(doc, 'Injectable');
3738
}
3839

39-
export function isNgModule(doc: ClassExportDoc) {
40+
export function isNgModule(doc: ClassExportDoc): boolean {
4041
return hasClassDecorator(doc, 'NgModule');
4142
}
4243

43-
export function isDeprecatedDoc(doc: any) {
44-
return ((doc.tags && doc.tags.tags) || []).some((tag: any) => tag.tagName === 'deprecated');
44+
export function isDeprecatedDoc(doc: ApiDoc): boolean {
45+
return hasJsDocTag(doc, 'deprecated');
4546
}
4647

4748
/** Whether the given document is annotated with the "@docs-primary-export" jsdoc tag. */
48-
export function isPrimaryExportDoc(doc: any) {
49-
return ((doc.tags && doc.tags.tags) || []).some(
50-
(tag: any) => tag.tagName === 'docs-primary-export',
51-
);
49+
export function isPrimaryExportDoc(doc: ApiDoc): boolean {
50+
return hasJsDocTag(doc, 'docs-primary-export');
5251
}
5352

54-
export function getDirectiveSelectors(classDoc: CategorizedClassDoc) {
53+
export function getDirectiveSelectors(classDoc: CategorizedClassDoc): string[] | undefined {
5554
if (classDoc.directiveMetadata) {
5655
const directiveSelectors: string = classDoc.directiveMetadata.get('selector');
5756

@@ -65,28 +64,24 @@ export function getDirectiveSelectors(classDoc: CategorizedClassDoc) {
6564
return undefined;
6665
}
6766

68-
export function hasMemberDecorator(doc: MemberDoc, decoratorName: string) {
67+
export function hasMemberDecorator(doc: MemberDoc, decoratorName: string): boolean {
6968
return doc.docType == 'member' && hasDecorator(doc, decoratorName);
7069
}
7170

72-
export function hasClassDecorator(doc: ClassExportDoc, decoratorName: string) {
71+
export function hasClassDecorator(doc: ClassExportDoc, decoratorName: string): boolean {
7372
return doc.docType == 'class' && hasDecorator(doc, decoratorName);
7473
}
7574

76-
export function hasDecorator(doc: HasDecoratorsDoc, decoratorName: string) {
75+
export function hasDecorator(doc: HasDecoratorsDoc, decoratorName: string): boolean {
7776
return (
7877
!!doc.decorators &&
7978
doc.decorators.length > 0 &&
8079
doc.decorators.some(d => d.name == decoratorName)
8180
);
8281
}
8382

84-
export function getBreakingChange(doc: any): string | null {
85-
if (!doc.tags) {
86-
return null;
87-
}
88-
89-
const breakingChange = doc.tags.tags.find((t: any) => t.tagName === 'breaking-change');
83+
export function getBreakingChange(doc: ApiDoc): string | null {
84+
const breakingChange = findJsDocTag(doc, 'breaking-change');
9085
return breakingChange ? breakingChange.description : null;
9186
}
9287

tools/dgeni/common/private-docs.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {ApiDoc} from 'dgeni-packages/typescript/api-doc-types/ApiDoc';
22
import {MemberDoc} from 'dgeni-packages/typescript/api-doc-types/MemberDoc';
33
import {isInheritanceCreatedDoc} from './class-inheritance';
4+
import {findJsDocTag, hasJsDocTag, Tag} from './tags';
45

56
const INTERNAL_METHODS = [
67
// Lifecycle methods
@@ -48,20 +49,18 @@ export function isPublicDoc(doc: ApiDoc) {
4849
}
4950

5051
/** Gets the @docs-public tag from the given document if present. */
51-
export function getDocsPublicTag(doc: any): {tagName: string; description: string} | undefined {
52-
const tags = doc.tags && doc.tags.tags;
53-
return tags ? tags.find((d: any) => d.tagName == 'docs-public') : undefined;
52+
export function getDocsPublicTag(doc: ApiDoc): Tag | undefined {
53+
return findJsDocTag(doc, 'docs-public');
5454
}
5555

5656
/** Whether the given method member is listed as an internal member. */
57-
function _isInternalMember(memberDoc: MemberDoc) {
57+
function _isInternalMember(memberDoc: MemberDoc): boolean {
5858
return INTERNAL_METHODS.includes(memberDoc.name);
5959
}
6060

6161
/** Whether the given doc has a @docs-private tag set. */
62-
function _hasDocsPrivateTag(doc: any) {
63-
const tags = doc.tags && doc.tags.tags;
64-
return tags ? tags.find((d: any) => d.tagName == 'docs-private') : false;
62+
function _hasDocsPrivateTag(doc: ApiDoc): boolean {
63+
return hasJsDocTag(doc, 'docs-private');
6564
}
6665

6766
/**
@@ -73,6 +72,6 @@ function _hasDocsPrivateTag(doc: any) {
7372
* split up into several base classes to support the MDC prototypes. e.g. "_MatMenu" should
7473
* show up in the docs as "MatMenu".
7574
*/
76-
function _isEnforcedPublicDoc(doc: any): boolean {
75+
function _isEnforcedPublicDoc(doc: ApiDoc): boolean {
7776
return getDocsPublicTag(doc) !== undefined;
7877
}

tools/dgeni/common/tags.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import {ApiDoc} from 'dgeni-packages/typescript/api-doc-types/ApiDoc';
2+
3+
/**
4+
* Type describing a collection of tags, matching with the objects
5+
* created by the Dgeni JSDoc processors.
6+
*
7+
* https://github.com/angular/dgeni-packages/blob/19e629c0d156572cbea149af9e0cc7ec02db7cb6/jsdoc/lib/TagCollection.js#L4
8+
*/
9+
export interface TagCollection {
10+
/** List of tags. */
11+
tags: Tag[];
12+
/** Map which maps tag names to their tag instances. */
13+
tagsByName: Map<string, Tag[]>;
14+
/** List of tags which are unkown, or have errors. */
15+
badTags: Tag[];
16+
}
17+
18+
/**
19+
* Type describing a tag, matching with the objects created by the
20+
* Dgeni JSDoc processors.
21+
*
22+
* https://github.com/angular/dgeni-packages/blob/19e629c0d156572cbea149af9e0cc7ec02db7cb6/jsdoc/lib/Tag.js#L1
23+
*/
24+
export interface Tag {
25+
/** Definition of the tag. Undefined if the tag is unknown. */
26+
tagDef: undefined | TagDefinition;
27+
/** Name of the tag (excluding the `@`) */
28+
tagName: string;
29+
/** Description associated with the tag. */
30+
description: string;
31+
/** Source file line where this tag starts. */
32+
startingLine: number;
33+
/** Optional list of errors that have been computed for this tag. */
34+
errors?: string[];
35+
}
36+
37+
/** Type describing a tag definition for the Dgeni JSDoc processor. */
38+
export interface TagDefinition {
39+
/** Name of the tag (excluding the `@`) */
40+
name: string;
41+
/** Property where the tag information should be attached to. */
42+
docProperty?: string;
43+
/** Whether multiple instances of the tag can be used in the same comment. */
44+
multi?: boolean;
45+
/** Whether this tag is required for all API documents. */
46+
required?: boolean;
47+
}
48+
49+
/** Type describing an API doc with JSDoc tag information. */
50+
export type ApiDocWithJsdocTags = ApiDoc & {
51+
/** Collection of JSDoc tags attached to this API document. */
52+
tags: TagCollection;
53+
};
54+
55+
/** Whether the specified API document has JSDoc tag information attached. */
56+
export function isApiDocWithJsdocTags(doc: ApiDoc): doc is ApiDocWithJsdocTags {
57+
return (doc as Partial<ApiDocWithJsdocTags>).tags !== undefined;
58+
}
59+
60+
/** Finds the specified JSDoc tag within the given API doc. */
61+
export function findJsDocTag(doc: ApiDoc, tagName: string): Tag | undefined {
62+
if (!isApiDocWithJsdocTags(doc)) {
63+
return undefined;
64+
}
65+
66+
return doc.tags.tags.find(t => t.tagName === tagName);
67+
}
68+
69+
/** Gets whether the specified API doc has a given JSDoc tag. */
70+
export function hasJsDocTag(doc: ApiDoc, tagName: string): boolean {
71+
return findJsDocTag(doc, tagName) !== undefined;
72+
}

tools/dgeni/docs-package.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {AsyncFunctionsProcessor} from './processors/async-functions';
88
import {categorizer} from './processors/categorizer';
99
import {DocsPrivateFilter} from './processors/docs-private-filter';
1010
import {EntryPointGrouper} from './processors/entry-point-grouper';
11+
import {ErrorUnknownJsdocTagsProcessor} from './processors/error-unknown-jsdoc-tags';
1112
import {FilterDuplicateExports} from './processors/filter-duplicate-exports';
1213
import {mergeInheritedProperties} from './processors/merge-inherited-properties';
1314
import {resolveInheritedDocs} from './processors/resolve-inherited-docs';
@@ -51,6 +52,9 @@ apiDocsPackage.processor(mergeInheritedProperties);
5152
// Processor that filters out symbols that should not be shown in the docs.
5253
apiDocsPackage.processor(new DocsPrivateFilter());
5354

55+
// Processor that throws an error if API docs with unknown JSDoc tags are discovered.
56+
apiDocsPackage.processor(new ErrorUnknownJsdocTagsProcessor());
57+
5458
// Processor that appends categorization flags to the docs, e.g. `isDirective`, `isNgModule`, etc.
5559
apiDocsPackage.processor(categorizer);
5660

@@ -100,6 +104,15 @@ apiDocsPackage.config(function (parseTagsProcessor: any) {
100104
{name: 'template', multi: true},
101105
// JSDoc annotations/tags which are not supported by default.
102106
{name: 'throws', multi: true},
107+
108+
// Annotations/tags from external API docs (i.e. from the node modules). These tags are
109+
// added so that no errors are reported.
110+
// TODO(devversion): remove this once the fix in dgeni-package is available.
111+
// https://github.com/angular/dgeni-packages/commit/19e629c0d156572cbea149af9e0cc7ec02db7cb6.
112+
{name: 'usageNotes'},
113+
{name: 'publicApi'},
114+
{name: 'ngModule', multi: true},
115+
{name: 'nodoc'},
103116
]);
104117
});
105118

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import {DocCollection, Processor} from 'dgeni';
2+
import {isApiDocWithJsdocTags} from '../common/tags';
3+
4+
/**
5+
* Processor that checks API docs for unknown JSDoc tags. Dgeni by default will
6+
* warn about unknown tags. This processor will throw an error instead.
7+
*/
8+
export class ErrorUnknownJsdocTagsProcessor implements Processor {
9+
name = 'error-unknown-tags';
10+
$runAfter = ['docs-private-filter'];
11+
$runBefore = ['categorizer'];
12+
13+
$process(docs: DocCollection) {
14+
for (const doc of docs) {
15+
if (!isApiDocWithJsdocTags(doc)) {
16+
continue;
17+
}
18+
19+
if (doc.tags.badTags.length > 0) {
20+
let errorMessage = `Found errors for processed JSDoc comments in ${doc.id}:\n`;
21+
22+
for (const tag of doc.tags.badTags) {
23+
errorMessage += '\n';
24+
25+
if (tag.tagDef === undefined) {
26+
errorMessage += ` * Tag "${tag.tagName}": Unknown tag.\n`;
27+
}
28+
29+
for (const concreteError of tag.errors ?? []) {
30+
errorMessage += ` * Tag "${tag.tagName}": ${concreteError}\n`;
31+
}
32+
}
33+
34+
throw new Error(errorMessage);
35+
}
36+
}
37+
}
38+
}

tools/dgeni/processors/resolve-inherited-docs.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ export function resolveInheritedDocs(exportSymbolsToDocsMap: Map<ts.Symbol, Clas
2020
* processed by other standard processors in the Dgeni pipeline. This is helpful as
2121
* API documents for inheritance are created manually if not exported, and we'd want
2222
* such docs to be processed by the Dgeni JSDoc processor for example.
23+
*
24+
* Note that we also want to include external API docs (e.g. from the node modules)
25+
* since members from those can also be merged with public-facing API docs.
2326
*/
2427
export class ResolveInheritedDocs implements Processor {
2528
$runBefore = ['docs-private-filter', 'parsing-tags'];
@@ -44,14 +47,6 @@ export class ResolveInheritedDocs implements Processor {
4447
if (!isInheritanceCreatedDoc(apiDoc)) {
4548
return;
4649
}
47-
// If this is an external document not part of our sources, we will not add it to the
48-
// Dgeni API doc pipeline. Docs would be filtered regardless, but adding them to the
49-
// pipeline could cause e.g. the JSDoc parser to complain about tags/annotations which
50-
// are not known/relevant to our API docs. e.g. Framework uses `@nodoc` or `@usageNotes`.
51-
if (apiDoc.fileInfo.projectRelativePath.startsWith('../')) {
52-
return;
53-
}
54-
5550
// Add the member docs for the inherited doc to the Dgeni doc collection.
5651
this._getContainingMemberDocs(apiDoc).forEach(d => newDocs.add(d));
5752
// Add the class-like export doc to the Dgeni doc collection.

0 commit comments

Comments
 (0)