Skip to content

Commit e2c24b3

Browse files
devversiontinayuangao
authored andcommitted
chore(dgeni): better extraction of directive metadata (#9387)
* chore(dgeni): better extraction of directive metadata * No longer extracts the directive/component metadata using Regular Expressions. * Fixes that inherited properties from interfaces or super-classes are not showing up as inputs/outputs if they are specified in the component/directive metadata. * Now handles multi-line selectors (this was not possible using the Regular Expression) * Fixes that merged inherited properties have a reference to the original document (this causes unexpected behavior; if properties are updated). References #9299
1 parent 9e919b8 commit e2c24b3

File tree

7 files changed

+166
-61
lines changed

7 files changed

+166
-61
lines changed

tools/dgeni/common/decorators.ts

Lines changed: 6 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import {ClassExportDoc} from 'dgeni-packages/typescript/api-doc-types/ClassExportDoc';
77
import {PropertyMemberDoc} from 'dgeni-packages/typescript/api-doc-types/PropertyMemberDoc';
88
import {MemberDoc} from 'dgeni-packages/typescript/api-doc-types/MemberDoc';
9+
import {CategorizedClassDoc} from './dgeni-definitions';
910

1011
const SELECTOR_BLACKLIST = new Set([
1112
'[portal]',
@@ -49,28 +50,16 @@ export function isNgModule(doc: ClassExportDoc) {
4950
return hasClassDecorator(doc, 'NgModule');
5051
}
5152

52-
export function isDirectiveOutput(doc: PropertyMemberDoc) {
53-
return hasMemberDecorator(doc, 'Output');
54-
}
55-
56-
export function isDirectiveInput(doc: PropertyMemberDoc) {
57-
return hasMemberDecorator(doc, 'Input');
58-
}
59-
6053
export function isDeprecatedDoc(doc: any) {
6154
return (doc.tags && doc.tags.tags || []).some((tag: any) => tag.tagName === 'deprecated');
6255
}
6356

64-
export function getDirectiveInputAlias(doc: PropertyMemberDoc) {
65-
return isDirectiveInput(doc) ? doc.decorators!.find(d => d.name == 'Input')!.arguments![0] : '';
66-
}
67-
68-
export function getDirectiveOutputAlias(doc: PropertyMemberDoc) {
69-
return isDirectiveOutput(doc) ? doc.decorators!.find(d => d.name == 'Output')!.arguments![0] : '';
70-
}
57+
export function getDirectiveSelectors(classDoc: CategorizedClassDoc) {
58+
if (!classDoc.directiveMetadata) {
59+
return;
60+
}
7161

72-
export function getDirectiveSelectors(classDoc: ClassExportDoc) {
73-
const directiveSelectors = getMetadataProperty(classDoc, 'selector');
62+
const directiveSelectors: string = classDoc.directiveMetadata.get('selector');
7463

7564
if (directiveSelectors) {
7665
// Filter blacklisted selectors and remove line-breaks in resolved selectors.
@@ -79,18 +68,6 @@ export function getDirectiveSelectors(classDoc: ClassExportDoc) {
7968
}
8069
}
8170

82-
export function getMetadataProperty(doc: ClassExportDoc, property: string) {
83-
const metadata = doc.decorators!
84-
.find(d => d.name === 'Component' || d.name === 'Directive')!.arguments![0];
85-
86-
// Use a Regex to determine the given metadata property. This is necessary, because we can't
87-
// parse the JSON due to environment variables inside of the JSON (e.g module.id)
88-
const matches = new RegExp(`${property}s*:\\s*(?:"|'|\`)((?:.|\\n|\\r)+?)(?:"|'|\`)`)
89-
.exec(metadata);
90-
91-
return matches && matches[1].trim();
92-
}
93-
9471
export function hasMemberDecorator(doc: MemberDoc, decoratorName: string) {
9572
return doc.docType == 'member' && hasDecorator(doc, decoratorName);
9673
}

tools/dgeni/common/dgeni-definitions.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export interface CategorizedClassDoc extends ClassExportDoc, CategorizedClassLik
1717
isNgModule: boolean;
1818
directiveExportAs?: string | null;
1919
directiveSelectors?: string[];
20+
directiveMetadata: Map<string, any> | null;
2021
extendedDoc: ClassLikeExportDoc | null;
2122
}
2223

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import {CategorizedClassDoc} from './dgeni-definitions';
2+
import {
3+
ArrayLiteralExpression,
4+
CallExpression,
5+
ObjectLiteralExpression,
6+
PropertyAssignment,
7+
StringLiteral, SyntaxKind
8+
} from 'typescript';
9+
10+
/**
11+
* Determines the component or directive metadata from the specified Dgeni class doc. The resolved
12+
* directive metadata will be stored in a Map.
13+
*
14+
* Currently only string literal assignments and array literal assignments are supported. Other
15+
* value types are not necessary because they are not needed for any user-facing documentation.
16+
*
17+
* ```ts
18+
* @Component({
19+
* inputs: ["red", "blue"],
20+
* exportAs: "test"
21+
* })
22+
* export class MyComponent {}
23+
* ```
24+
*/
25+
export function getDirectiveMetadata(classDoc: CategorizedClassDoc): Map<string, any> | null {
26+
const declaration = classDoc.symbol.valueDeclaration;
27+
28+
if (!declaration || !declaration.decorators) {
29+
return null;
30+
}
31+
32+
const directiveDecorator = declaration.decorators
33+
.filter(decorator => decorator.expression)
34+
.filter(decorator => decorator.expression.kind === SyntaxKind.CallExpression)
35+
.find(decorator => (decorator.expression as any).expression.getText() === 'Component' ||
36+
(decorator.expression as any).expression.getText() === 'Directive');
37+
38+
if (!directiveDecorator) {
39+
return null;
40+
}
41+
42+
// Since the actual decorator expression is by default a LeftHandSideExpression, and TypeScript
43+
// doesn't allow a casting it to a CallExpression, we have to cast it to "any" before.
44+
const expression = (directiveDecorator.expression as any) as CallExpression;
45+
46+
// The argument length of the CallExpression needs to be exactly one, because it's the single
47+
// JSON object in the @Component/@Directive decorator.
48+
if (expression.arguments.length !== 1) {
49+
return null;
50+
}
51+
52+
const objectExpression = expression.arguments[0] as ObjectLiteralExpression;
53+
const resultMetadata = new Map<string, any>();
54+
55+
objectExpression.properties.forEach((prop: PropertyAssignment) => {
56+
57+
// Support ArrayLiteralExpression assignments in the directive metadata.
58+
if (prop.initializer.kind === SyntaxKind.ArrayLiteralExpression) {
59+
const arrayData = (prop.initializer as ArrayLiteralExpression).elements
60+
.map((literal: StringLiteral) => literal.text);
61+
62+
resultMetadata.set(prop.name.getText(), arrayData);
63+
}
64+
65+
// Support normal StringLiteral and NoSubstitutionTemplateLiteral assignments
66+
if (prop.initializer.kind === SyntaxKind.StringLiteral ||
67+
prop.initializer.kind === SyntaxKind.NoSubstitutionTemplateLiteral) {
68+
resultMetadata.set(prop.name.getText(), (prop.initializer as StringLiteral).text);
69+
}
70+
});
71+
72+
return resultMetadata;
73+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import {PropertyMemberDoc} from 'dgeni-packages/typescript/api-doc-types/PropertyMemberDoc';
2+
import {hasMemberDecorator} from './decorators';
3+
4+
/** Interface that describes an Angular property binding. Can be either an input or output. */
5+
export interface PropertyBinding {
6+
name: string;
7+
alias?: string;
8+
}
9+
10+
/**
11+
* Detects whether the specified property member is an input. If the property is an input, the
12+
* alias and input name will be returned.
13+
*/
14+
export function getInputBindingData(doc: PropertyMemberDoc, metadata: Map<string, any>)
15+
: PropertyBinding | undefined {
16+
return getBindingPropertyData(doc, metadata, 'inputs', 'Input');
17+
}
18+
19+
/**
20+
* Detects whether the specified property member is an output. If the property is an output, the
21+
* alias and output name will be returned.
22+
*/
23+
export function getOutputBindingData(doc: PropertyMemberDoc, metadata: Map<string, any>)
24+
: PropertyBinding | undefined {
25+
return getBindingPropertyData(doc, metadata, 'outputs', 'Output');
26+
}
27+
28+
/**
29+
* Method that detects the specified type of property binding (either "output" or "input") from
30+
* the directive metadata or from the associated decorator on the property.
31+
*/
32+
function getBindingPropertyData(doc: PropertyMemberDoc, metadata: Map<string, any>,
33+
propertyName: string, decoratorName: string) {
34+
35+
if (metadata) {
36+
const metadataValues: string[] = metadata.get(propertyName) || [];
37+
const foundValue = metadataValues.find(value => value.split(':')[0] === doc.name);
38+
39+
if (foundValue) {
40+
return {
41+
name: doc.name,
42+
alias: foundValue.split(':')[1]
43+
};
44+
}
45+
}
46+
47+
if (hasMemberDecorator(doc, decoratorName)) {
48+
return {
49+
name: doc.name,
50+
alias: doc.decorators!.find(d => d.name == decoratorName)!.arguments![0]
51+
};
52+
}
53+
}

tools/dgeni/common/sort-members.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import {isDirectiveInput, isDirectiveOutput} from './decorators';
21
import {CategorizedMethodMemberDoc, CategorizedPropertyMemberDoc} from './dgeni-definitions';
32

43
/** Combined type for a categorized method member document. */
@@ -16,13 +15,13 @@ export function sortCategorizedMembers(docA: CategorizedMemberDoc, docB: Categor
1615
}
1716

1817
// Sort in the order of: Inputs, Outputs, neither
19-
if ((isDirectiveInput(docA) && !isDirectiveInput(docB)) ||
20-
(isDirectiveOutput(docA) && !isDirectiveInput(docB) && !isDirectiveOutput(docB))) {
18+
if ((docA.isDirectiveInput && !docB.isDirectiveInput) ||
19+
(docA.isDirectiveOutput && !docB.isDirectiveInput && !docB.isDirectiveOutput)) {
2120
return -1;
2221
}
2322

24-
if ((isDirectiveInput(docB) && !isDirectiveInput(docA)) ||
25-
(isDirectiveOutput(docB) && !isDirectiveInput(docA) && !isDirectiveOutput(docA))) {
23+
if ((docB.isDirectiveInput && !docA.isDirectiveInput) ||
24+
(docB.isDirectiveOutput && !docA.isDirectiveInput && !docA.isDirectiveOutput)) {
2625
return 1;
2726
}
2827

tools/dgeni/processors/categorizer.ts

Lines changed: 21 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,16 @@
11
import {DocCollection, Processor} from 'dgeni';
22
import {MethodMemberDoc} from 'dgeni-packages/typescript/api-doc-types/MethodMemberDoc';
3+
import {getDirectiveMetadata} from '../common/directive-metadata';
34
import {
4-
decorateDeprecatedDoc,
5-
getDirectiveInputAlias,
6-
getDirectiveOutputAlias,
7-
getDirectiveSelectors,
8-
getMetadataProperty,
9-
isDirective,
10-
isDirectiveInput,
11-
isDirectiveOutput,
12-
isMethod,
13-
isNgModule,
14-
isProperty,
5+
decorateDeprecatedDoc, getDirectiveSelectors, isDirective, isMethod, isNgModule, isProperty,
156
isService
167
} from '../common/decorators';
178
import {
18-
CategorizedClassDoc,
19-
CategorizedClassLikeDoc,
20-
CategorizedMethodMemberDoc,
9+
CategorizedClassDoc, CategorizedClassLikeDoc, CategorizedMethodMemberDoc,
2110
CategorizedPropertyMemberDoc
2211
} from '../common/dgeni-definitions';
2312
import {normalizeMethodParameters} from '../common/normalize-method-parameters';
13+
import {getInputBindingData, getOutputBindingData} from '../common/property-bindings';
2414
import {sortCategorizedMembers} from '../common/sort-members';
2515

2616

@@ -56,6 +46,11 @@ export class Categorizer implements Processor {
5646
.filter(isProperty)
5747
.filter(filterDuplicateMembers) as CategorizedPropertyMemberDoc[];
5848

49+
// Special decorations for real class documents that don't apply for interfaces.
50+
if (classLikeDoc.docType === 'class') {
51+
this.decorateClassDoc(classLikeDoc as CategorizedClassDoc);
52+
}
53+
5954
// Call decorate hooks that can modify the method and property docs.
6055
classLikeDoc.methods.forEach(doc => this.decorateMethodDoc(doc));
6156
classLikeDoc.properties.forEach(doc => this.decoratePropertyDoc(doc));
@@ -65,11 +60,6 @@ export class Categorizer implements Processor {
6560
// Sort members
6661
classLikeDoc.methods.sort(sortCategorizedMembers);
6762
classLikeDoc.properties.sort(sortCategorizedMembers);
68-
69-
// Special decorations for real class documents that don't apply for interfaces.
70-
if (classLikeDoc.docType === 'class') {
71-
this.decorateClassDoc(classLikeDoc as CategorizedClassDoc);
72-
}
7363
}
7464

7565
/**
@@ -82,11 +72,12 @@ export class Categorizer implements Processor {
8272
// clauses for the Dgeni document. To make the template syntax simpler and more readable,
8373
// store the extended class in a variable.
8474
classDoc.extendedDoc = classDoc.extendsClauses[0] ? classDoc.extendsClauses[0].doc! : null;
75+
classDoc.directiveMetadata = getDirectiveMetadata(classDoc);
8576

8677
// Categorize the current visited classDoc into its Angular type.
87-
if (isDirective(classDoc)) {
78+
if (isDirective(classDoc) && classDoc.directiveMetadata) {
8879
classDoc.isDirective = true;
89-
classDoc.directiveExportAs = getMetadataProperty(classDoc, 'exportAs');
80+
classDoc.directiveExportAs = classDoc.directiveMetadata.get('exportAs');
9081
classDoc.directiveSelectors = getDirectiveSelectors(classDoc);
9182
} else if (isService(classDoc)) {
9283
classDoc.isService = true;
@@ -114,13 +105,17 @@ export class Categorizer implements Processor {
114105
private decoratePropertyDoc(propertyDoc: CategorizedPropertyMemberDoc) {
115106
decorateDeprecatedDoc(propertyDoc);
116107

117-
// TODO(devversion): detect inputs based on the `inputs` property in the component metadata.
108+
const metadata = propertyDoc.containerDoc.docType === 'class' ?
109+
(propertyDoc.containerDoc as CategorizedClassDoc).directiveMetadata : null;
110+
111+
const inputMetadata = metadata ? getInputBindingData(propertyDoc, metadata) : null;
112+
const outputMetadata = metadata ? getOutputBindingData(propertyDoc, metadata) : null;
118113

119-
propertyDoc.isDirectiveInput = isDirectiveInput(propertyDoc);
120-
propertyDoc.directiveInputAlias = getDirectiveInputAlias(propertyDoc);
114+
propertyDoc.isDirectiveInput = !!inputMetadata;
115+
propertyDoc.directiveInputAlias = (inputMetadata && inputMetadata.alias) || '';
121116

122-
propertyDoc.isDirectiveOutput = isDirectiveOutput(propertyDoc);
123-
propertyDoc.directiveOutputAlias = getDirectiveOutputAlias(propertyDoc);
117+
propertyDoc.isDirectiveOutput = !!outputMetadata;
118+
propertyDoc.directiveOutputAlias = (outputMetadata && outputMetadata.alias) || '';
124119
}
125120
}
126121

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,14 @@ export class MergeInheritedProperties implements Processor {
2828

2929
private addMemberDocIfNotPresent(destination: ClassExportDoc, memberDoc: MemberDoc) {
3030
if (!destination.members.find(member => member.name === memberDoc.name)) {
31-
destination.members.push(memberDoc);
31+
// To be able to differentiate between member docs from the heritage clause and the
32+
// member doc for the destination class, we clone the member doc. It's important to keep
33+
// the prototype and reference because later, Dgeni identifies members and properties
34+
// by using an instance comparison.
35+
const newMemberDoc = Object.assign(Object.create(memberDoc), memberDoc);
36+
newMemberDoc.containerDoc = destination;
37+
38+
destination.members.push(newMemberDoc);
3239
}
3340
}
3441
}

0 commit comments

Comments
 (0)