Skip to content

Commit a909f81

Browse files
devversionmmalerba
authored andcommitted
build: do not merge interface members with class API members (#18088)
Currently we always merge _all_ members defined by interfaces into the members of Dgeni class API documents. This causes optional interface members to show up even if they are not implemented. To fix this, the member merging processor has been reworked to not even ever consider interfaces for the API docs. Instead, only members which are "extended" should show up. Support for mixins using intersection types has been added. Fixes #17148. Fixes #18014.
1 parent 02c24ec commit a909f81

File tree

5 files changed

+116
-24
lines changed

5 files changed

+116
-24
lines changed
Lines changed: 75 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,80 @@
1+
// tslint:disable:no-bitwise
2+
3+
import {ClassExportDoc} from 'dgeni-packages/typescript/api-doc-types/ClassExportDoc';
14
import {ClassLikeExportDoc} from 'dgeni-packages/typescript/api-doc-types/ClassLikeExportDoc';
5+
import {InterfaceExportDoc} from 'dgeni-packages/typescript/api-doc-types/InterfaceExportDoc';
6+
import * as ts from 'typescript';
27

38
/** Gets all class like export documents which the given doc inherits from. */
4-
export function getInheritedDocsOfClass(doc: ClassLikeExportDoc): ClassLikeExportDoc[] {
5-
const directBaseDocs = [
6-
...doc.implementsClauses.filter(clause => clause.doc).map(d => d.doc!),
7-
...doc.extendsClauses.filter(clause => clause.doc).map(d => d.doc!),
8-
];
9+
export function getInheritedDocsOfClass(
10+
doc: ClassLikeExportDoc,
11+
exportSymbolsToDocsMap: Map<ts.Symbol, ClassLikeExportDoc>): ClassLikeExportDoc[] {
12+
const result: ClassLikeExportDoc[] = [];
13+
const typeChecker = doc.typeChecker;
14+
for (let info of doc.extendsClauses) {
15+
if (info.doc) {
16+
result.push(info.doc, ...getInheritedDocsOfClass(info.doc, exportSymbolsToDocsMap));
17+
} else if (info.type) {
18+
// If the heritage info has not been resolved to a Dgeni API document, we try to
19+
// interpret the type expression and resolve/create corresponding Dgeni API documents.
20+
// An example is the use of mixins. Type-wise mixins are not like real classes, because
21+
// they are composed through an intersection type. In order to handle this pattern, we
22+
// need to handle intersection types manually and resolve them to Dgeni API documents.
23+
const resolvedType = typeChecker.getTypeAtLocation(info.type);
24+
const docs = getClassLikeDocsFromType(resolvedType, doc, exportSymbolsToDocsMap);
25+
// Add direct class-like types resolved from the expression.
26+
result.push(...docs);
27+
// Resolve inherited docs of the resolved documents.
28+
docs.forEach(d => result.push(...getInheritedDocsOfClass(d, exportSymbolsToDocsMap)));
29+
}
30+
}
31+
return result;
32+
}
33+
34+
/**
35+
* Gets all class-like Dgeni documents from the given type. e.g. intersection types of
36+
* multiple classes will result in multiple Dgeni API documents for each class.
37+
*/
38+
function getClassLikeDocsFromType(
39+
type: ts.Type, baseDoc: ClassLikeExportDoc,
40+
exportSymbolsToDocsMap: Map<ts.Symbol, ClassLikeExportDoc>): ClassLikeExportDoc[] {
41+
let aliasSymbol: ts.Symbol|undefined = undefined;
42+
let symbol: ts.Symbol = type.symbol;
43+
const typeChecker = baseDoc.typeChecker;
44+
45+
// Symbols can be aliases of the declaration symbol. e.g. in named import
46+
// specifiers. We need to resolve the aliased symbol back to the declaration symbol.
47+
if (symbol && (symbol.flags & ts.SymbolFlags.Alias) !== 0) {
48+
aliasSymbol = symbol;
49+
symbol = typeChecker.getAliasedSymbol(symbol);
50+
}
951

10-
return [
11-
...directBaseDocs,
12-
// recursively collect base documents of direct base documents.
13-
...directBaseDocs.reduce(
14-
(res: ClassLikeExportDoc[], d) => res.concat(getInheritedDocsOfClass(d)), []),
15-
];
52+
// Intersection types are commonly used in TypeScript mixins to express the
53+
// class augmentation. e.g. "BaseClass & CanColor".
54+
if (type.isIntersection()) {
55+
return type.types.reduce(
56+
(res, t) => [...res, ...getClassLikeDocsFromType(t, baseDoc, exportSymbolsToDocsMap)],
57+
[] as ClassLikeExportDoc[]);
58+
} else if (symbol) {
59+
// If the given symbol has already been registered within Dgeni, we use the
60+
// existing symbol instead of creating a new one. The dgeni typescript package
61+
// keeps track of all exported symbols and their corresponding docs. See:
62+
// dgeni-packages/blob/master/typescript/src/processors/linkInheritedDocs.ts
63+
if (exportSymbolsToDocsMap.has(symbol)) {
64+
return [exportSymbolsToDocsMap.get(symbol)!];
65+
}
66+
let createdDoc: ClassLikeExportDoc|null = null;
67+
if ((symbol.flags & ts.SymbolFlags.Class) !== 0) {
68+
createdDoc = new ClassExportDoc(baseDoc.host, baseDoc.moduleDoc, symbol, aliasSymbol);
69+
} else if ((symbol.flags & ts.SymbolFlags.Interface) !== 0) {
70+
createdDoc = new InterfaceExportDoc(baseDoc.host, baseDoc.moduleDoc, symbol, aliasSymbol);
71+
}
72+
// If a new document has been created, add it to the shared symbol
73+
// docs map and return it.
74+
if (createdDoc) {
75+
exportSymbolsToDocsMap.set(aliasSymbol || symbol, createdDoc);
76+
return [createdDoc];
77+
}
78+
}
79+
return [];
1680
}

tools/dgeni/docs-package.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import {Package} from 'dgeni';
2+
import {ReadTypeScriptModules} from 'dgeni-packages/typescript/processors/readTypeScriptModules';
23
import {Host} from 'dgeni-packages/typescript/services/ts-host/host';
4+
import {TypeFormatFlags} from 'typescript';
35
import {HighlightNunjucksExtension} from './nunjucks-tags/highlight';
46
import {patchLogService} from './patch-log-service';
57
import {AsyncFunctionsProcessor} from './processors/async-functions';
8+
import {categorizer} from './processors/categorizer';
69
import {DocsPrivateFilter} from './processors/docs-private-filter';
7-
import {Categorizer} from './processors/categorizer';
8-
import {FilterDuplicateExports} from './processors/filter-duplicate-exports';
9-
import {MergeInheritedProperties} from './processors/merge-inherited-properties';
1010
import {EntryPointGrouper} from './processors/entry-point-grouper';
11-
import {ReadTypeScriptModules} from 'dgeni-packages/typescript/processors/readTypeScriptModules';
12-
import {TypeFormatFlags} from 'typescript';
11+
import {FilterDuplicateExports} from './processors/filter-duplicate-exports';
12+
import {mergeInheritedProperties} from './processors/merge-inherited-properties';
1313

1414
// Dgeni packages that the Material docs package depends on.
1515
const jsdocPackage = require('dgeni-packages/jsdoc');
@@ -39,13 +39,14 @@ export const apiDocsPackage = new Package('material2-api-docs', [
3939
apiDocsPackage.processor(new FilterDuplicateExports());
4040

4141
// Processor that merges inherited properties of a class with the class doc.
42-
apiDocsPackage.processor(new MergeInheritedProperties());
42+
// Note: needs to use a factory function since the processor relies on DI.
43+
apiDocsPackage.processor(mergeInheritedProperties);
4344

4445
// Processor that filters out symbols that should not be shown in the docs.
4546
apiDocsPackage.processor(new DocsPrivateFilter());
4647

4748
// Processor that appends categorization flags to the docs, e.g. `isDirective`, `isNgModule`, etc.
48-
apiDocsPackage.processor(new Categorizer());
49+
apiDocsPackage.processor(categorizer);
4950

5051
// Processor to group docs into top-level entry-points such as "tabs", "sidenav", etc.
5152
apiDocsPackage.processor(new EntryPointGrouper());

tools/dgeni/processors/categorizer.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import {DocCollection, Processor} from 'dgeni';
2+
import {ClassLikeExportDoc} from 'dgeni-packages/typescript/api-doc-types/ClassLikeExportDoc';
23
import {MemberDoc} from 'dgeni-packages/typescript/api-doc-types/MemberDoc';
4+
import * as ts from 'typescript';
35
import {getInheritedDocsOfClass} from '../common/class-inheritance';
46
import {
57
decorateDeprecatedDoc,
@@ -25,6 +27,14 @@ import {isPublicDoc} from '../common/private-docs';
2527
import {getInputBindingData, getOutputBindingData} from '../common/property-bindings';
2628
import {sortCategorizedMethodMembers, sortCategorizedPropertyMembers} from '../common/sort-members';
2729

30+
/**
31+
* Factory function for the "Categorizer" processor. Dgeni does not support
32+
* dependency injection for classes. The symbol docs map is provided by the
33+
* TypeScript dgeni package.
34+
*/
35+
export function categorizer(exportSymbolsToDocsMap: Map<ts.Symbol, ClassLikeExportDoc>) {
36+
return new Categorizer(exportSymbolsToDocsMap);
37+
}
2838

2939
/**
3040
* Processor to add properties to docs objects.
@@ -35,9 +45,12 @@ import {sortCategorizedMethodMembers, sortCategorizedPropertyMembers} from '../c
3545
* isNgModule | Whether the doc is for an NgModule
3646
*/
3747
export class Categorizer implements Processor {
38-
name = 'categorizer';
3948
$runBefore = ['docs-processed', 'entryPointGrouper'];
4049

50+
constructor(
51+
/** Shared map that can be used to resolve docs through symbols. */
52+
private _exportSymbolsToDocsMap: Map<ts.Symbol, ClassLikeExportDoc>) {}
53+
4154
$process(docs: DocCollection) {
4255
docs.filter(doc => doc.docType === 'class' || doc.docType === 'interface')
4356
.forEach(doc => this._decorateClassLikeDoc(doc));
@@ -93,7 +106,7 @@ export class Categorizer implements Processor {
93106
// store the extended class in a variable.
94107
classDoc.extendedDoc = classDoc.extendsClauses[0] ? classDoc.extendsClauses[0].doc! : undefined;
95108
classDoc.directiveMetadata = getDirectiveMetadata(classDoc);
96-
classDoc.inheritedDocs = getInheritedDocsOfClass(classDoc);
109+
classDoc.inheritedDocs = getInheritedDocsOfClass(classDoc, this._exportSymbolsToDocsMap);
97110

98111
// In case the extended document is not public, we don't want to print it in the
99112
// rendered class API doc. This causes confusion and also is not helpful as the

tools/dgeni/processors/docs-private-filter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {getDocsPublicTag, isPublicDoc} from '../common/private-docs';
99
export class DocsPrivateFilter implements Processor {
1010
name = 'docs-private-filter';
1111
$runBefore = ['categorizer'];
12-
$runAfter = ['merge-inherited-properties'];
12+
$runAfter = ['mergeInheritedProperties'];
1313

1414
$process(docs: DocCollection) {
1515
return docs.filter(doc => {

tools/dgeni/processors/merge-inherited-properties.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,31 @@
11
import {DocCollection, Processor} from 'dgeni';
22
import {ClassExportDoc} from 'dgeni-packages/typescript/api-doc-types/ClassExportDoc';
3+
import {ClassLikeExportDoc} from 'dgeni-packages/typescript/api-doc-types/ClassLikeExportDoc';
34
import {MemberDoc} from 'dgeni-packages/typescript/api-doc-types/MemberDoc';
5+
import * as ts from 'typescript';
46
import {getInheritedDocsOfClass} from '../common/class-inheritance';
57

8+
/**
9+
* Factory function for the "MergeInheritedProperties" processor. Dgeni does not support
10+
* dependency injection for classes. The symbol docs map is provided by the TypeScript
11+
* dgeni package.
12+
*/
13+
export function mergeInheritedProperties(
14+
exportSymbolsToDocsMap: Map<ts.Symbol, ClassLikeExportDoc>) {
15+
return new MergeInheritedProperties(exportSymbolsToDocsMap);
16+
}
17+
618
/**
719
* Processor that merges inherited properties of a class with the class doc. This is necessary
820
* to properly show public properties from TypeScript mixin interfaces in the API.
921
*/
1022
export class MergeInheritedProperties implements Processor {
11-
name = 'merge-inherited-properties';
1223
$runBefore = ['categorizer'];
1324

25+
constructor(
26+
/** Shared map that can be used to resolve docs through symbols. */
27+
private _exportSymbolsToDocsMap: Map<ts.Symbol, ClassLikeExportDoc>) {}
28+
1429
$process(docs: DocCollection) {
1530
return docs.filter(doc => doc.docType === 'class')
1631
.forEach(doc => this._addInheritedProperties(doc));
@@ -19,7 +34,7 @@ export class MergeInheritedProperties implements Processor {
1934
private _addInheritedProperties(doc: ClassExportDoc) {
2035
// Note that we need to get check all base documents. We cannot assume
2136
// that directive base documents already have merged inherited members.
22-
getInheritedDocsOfClass(doc).forEach(d => {
37+
getInheritedDocsOfClass(doc, this._exportSymbolsToDocsMap).forEach(d => {
2338
d.members.forEach(member => {
2439
// only add inherited class members which are not "protected" or "private".
2540
if (member.accessibility === 'public') {
@@ -38,7 +53,6 @@ export class MergeInheritedProperties implements Processor {
3853
// tslint:disable-next-line:ban Need to use Object.assign to preserve the prototype.
3954
const newMemberDoc = Object.assign(Object.create(memberDoc), memberDoc);
4055
newMemberDoc.containerDoc = destination;
41-
4256
destination.members.push(newMemberDoc);
4357
}
4458
}

0 commit comments

Comments
 (0)