Skip to content

Commit 6529bea

Browse files
committed
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 78f49df commit 6529bea

File tree

7 files changed

+169
-61
lines changed

7 files changed

+169
-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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {ClassExportDoc} from 'dgeni-packages/typescript/api-doc-types/ClassExportDoc';
22
import {ClassLikeExportDoc} from 'dgeni-packages/typescript/api-doc-types/ClassLikeExportDoc';
33
import {PropertyMemberDoc} from 'dgeni-packages/typescript/api-doc-types/PropertyMemberDoc';
4+
import {DirectiveMetadata} from './directive-metadata';
45
import {NormalizedMethodMemberDoc} from './normalize-method-parameters';
56

67
/** Extended Dgeni class-like document that includes separated class members. */
@@ -17,6 +18,7 @@ export interface CategorizedClassDoc extends ClassExportDoc, CategorizedClassLik
1718
isNgModule: boolean;
1819
directiveExportAs?: string | null;
1920
directiveSelectors?: string[];
21+
directiveMetadata: DirectiveMetadata | null;
2022
extendedDoc: ClassLikeExportDoc | null;
2123
}
2224

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

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)