Skip to content

chore(dgeni): better extraction of directive metadata #9387

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jan 24, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 6 additions & 29 deletions tools/dgeni/common/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import {ClassExportDoc} from 'dgeni-packages/typescript/api-doc-types/ClassExportDoc';
import {PropertyMemberDoc} from 'dgeni-packages/typescript/api-doc-types/PropertyMemberDoc';
import {MemberDoc} from 'dgeni-packages/typescript/api-doc-types/MemberDoc';
import {CategorizedClassDoc} from './dgeni-definitions';

const SELECTOR_BLACKLIST = new Set([
'[portal]',
Expand Down Expand Up @@ -49,28 +50,16 @@ export function isNgModule(doc: ClassExportDoc) {
return hasClassDecorator(doc, 'NgModule');
}

export function isDirectiveOutput(doc: PropertyMemberDoc) {
return hasMemberDecorator(doc, 'Output');
}

export function isDirectiveInput(doc: PropertyMemberDoc) {
return hasMemberDecorator(doc, 'Input');
}

export function isDeprecatedDoc(doc: any) {
return (doc.tags && doc.tags.tags || []).some((tag: any) => tag.tagName === 'deprecated');
}

export function getDirectiveInputAlias(doc: PropertyMemberDoc) {
return isDirectiveInput(doc) ? doc.decorators!.find(d => d.name == 'Input')!.arguments![0] : '';
}

export function getDirectiveOutputAlias(doc: PropertyMemberDoc) {
return isDirectiveOutput(doc) ? doc.decorators!.find(d => d.name == 'Output')!.arguments![0] : '';
}
export function getDirectiveSelectors(classDoc: CategorizedClassDoc) {
if (!classDoc.directiveMetadata) {
return;
}

export function getDirectiveSelectors(classDoc: ClassExportDoc) {
const directiveSelectors = getMetadataProperty(classDoc, 'selector');
const directiveSelectors: string = classDoc.directiveMetadata.get('selector');

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

export function getMetadataProperty(doc: ClassExportDoc, property: string) {
const metadata = doc.decorators!
.find(d => d.name === 'Component' || d.name === 'Directive')!.arguments![0];

// Use a Regex to determine the given metadata property. This is necessary, because we can't
// parse the JSON due to environment variables inside of the JSON (e.g module.id)
const matches = new RegExp(`${property}s*:\\s*(?:"|'|\`)((?:.|\\n|\\r)+?)(?:"|'|\`)`)
.exec(metadata);

return matches && matches[1].trim();
}

export function hasMemberDecorator(doc: MemberDoc, decoratorName: string) {
return doc.docType == 'member' && hasDecorator(doc, decoratorName);
}
Expand Down
1 change: 1 addition & 0 deletions tools/dgeni/common/dgeni-definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export interface CategorizedClassDoc extends ClassExportDoc, CategorizedClassLik
isNgModule: boolean;
directiveExportAs?: string | null;
directiveSelectors?: string[];
directiveMetadata: Map<string, any> | null;
extendedDoc: ClassLikeExportDoc | null;
}

Expand Down
73 changes: 73 additions & 0 deletions tools/dgeni/common/directive-metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import {CategorizedClassDoc} from './dgeni-definitions';
import {
ArrayLiteralExpression,
CallExpression,
ObjectLiteralExpression,
PropertyAssignment,
StringLiteral, SyntaxKind
} from 'typescript';

/**
* Determines the component or directive metadata from the specified Dgeni class doc. The resolved
* directive metadata will be stored in a Map.
*
* Currently only string literal assignments and array literal assignments are supported. Other
* value types are not necessary because they are not needed for any user-facing documentation.
*
* ```ts
* @Component({
* inputs: ["red", "blue"],
* exportAs: "test"
* })
* export class MyComponent {}
* ```
*/
export function getDirectiveMetadata(classDoc: CategorizedClassDoc): Map<string, any> | null {
const declaration = classDoc.symbol.valueDeclaration;

if (!declaration || !declaration.decorators) {
return null;
}

const directiveDecorator = declaration.decorators
.filter(decorator => decorator.expression)
.filter(decorator => decorator.expression.kind === SyntaxKind.CallExpression)
.find(decorator => (decorator.expression as any).expression.getText() === 'Component' ||
(decorator.expression as any).expression.getText() === 'Directive');

if (!directiveDecorator) {
return null;
}

// Since the actual decorator expression is by default a LeftHandSideExpression, and TypeScript
// doesn't allow a casting it to a CallExpression, we have to cast it to "any" before.
const expression = (directiveDecorator.expression as any) as CallExpression;

// The argument length of the CallExpression needs to be exactly one, because it's the single
// JSON object in the @Component/@Directive decorator.
if (expression.arguments.length !== 1) {
return null;
}

const objectExpression = expression.arguments[0] as ObjectLiteralExpression;
const resultMetadata = new Map<string, any>();

objectExpression.properties.forEach((prop: PropertyAssignment) => {

// Support ArrayLiteralExpression assignments in the directive metadata.
if (prop.initializer.kind === SyntaxKind.ArrayLiteralExpression) {
const arrayData = (prop.initializer as ArrayLiteralExpression).elements
.map((literal: StringLiteral) => literal.text);

resultMetadata.set(prop.name.getText(), arrayData);
}

// Support normal StringLiteral and NoSubstitutionTemplateLiteral assignments
if (prop.initializer.kind === SyntaxKind.StringLiteral ||
prop.initializer.kind === SyntaxKind.NoSubstitutionTemplateLiteral) {
resultMetadata.set(prop.name.getText(), (prop.initializer as StringLiteral).text);
}
});

return resultMetadata;
}
53 changes: 53 additions & 0 deletions tools/dgeni/common/property-bindings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import {PropertyMemberDoc} from 'dgeni-packages/typescript/api-doc-types/PropertyMemberDoc';
import {hasMemberDecorator} from './decorators';

/** Interface that describes an Angular property binding. Can be either an input or output. */
export interface PropertyBinding {
name: string;
alias?: string;
}

/**
* Detects whether the specified property member is an input. If the property is an input, the
* alias and input name will be returned.
*/
export function getInputBindingData(doc: PropertyMemberDoc, metadata: Map<string, any>)
: PropertyBinding | undefined {
return getBindingPropertyData(doc, metadata, 'inputs', 'Input');
}

/**
* Detects whether the specified property member is an output. If the property is an output, the
* alias and output name will be returned.
*/
export function getOutputBindingData(doc: PropertyMemberDoc, metadata: Map<string, any>)
: PropertyBinding | undefined {
return getBindingPropertyData(doc, metadata, 'outputs', 'Output');
}

/**
* Method that detects the specified type of property binding (either "output" or "input") from
* the directive metadata or from the associated decorator on the property.
*/
function getBindingPropertyData(doc: PropertyMemberDoc, metadata: Map<string, any>,
propertyName: string, decoratorName: string) {

if (metadata) {
const metadataValues: string[] = metadata.get(propertyName) || [];
const foundValue = metadataValues.find(value => value.split(':')[0] === doc.name);

if (foundValue) {
return {
name: doc.name,
alias: foundValue.split(':')[1]
};
}
}

if (hasMemberDecorator(doc, decoratorName)) {
return {
name: doc.name,
alias: doc.decorators!.find(d => d.name == decoratorName)!.arguments![0]
};
}
}
9 changes: 4 additions & 5 deletions tools/dgeni/common/sort-members.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import {isDirectiveInput, isDirectiveOutput} from './decorators';
import {CategorizedMethodMemberDoc, CategorizedPropertyMemberDoc} from './dgeni-definitions';

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

// Sort in the order of: Inputs, Outputs, neither
if ((isDirectiveInput(docA) && !isDirectiveInput(docB)) ||
(isDirectiveOutput(docA) && !isDirectiveInput(docB) && !isDirectiveOutput(docB))) {
if ((docA.isDirectiveInput && !docB.isDirectiveInput) ||
(docA.isDirectiveOutput && !docB.isDirectiveInput && !docB.isDirectiveOutput)) {
return -1;
}

if ((isDirectiveInput(docB) && !isDirectiveInput(docA)) ||
(isDirectiveOutput(docB) && !isDirectiveInput(docA) && !isDirectiveOutput(docA))) {
if ((docB.isDirectiveInput && !docA.isDirectiveInput) ||
(docB.isDirectiveOutput && !docA.isDirectiveInput && !docA.isDirectiveOutput)) {
return 1;
}

Expand Down
47 changes: 21 additions & 26 deletions tools/dgeni/processors/categorizer.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,16 @@
import {DocCollection, Processor} from 'dgeni';
import {MethodMemberDoc} from 'dgeni-packages/typescript/api-doc-types/MethodMemberDoc';
import {getDirectiveMetadata} from '../common/directive-metadata';
import {
decorateDeprecatedDoc,
getDirectiveInputAlias,
getDirectiveOutputAlias,
getDirectiveSelectors,
getMetadataProperty,
isDirective,
isDirectiveInput,
isDirectiveOutput,
isMethod,
isNgModule,
isProperty,
decorateDeprecatedDoc, getDirectiveSelectors, isDirective, isMethod, isNgModule, isProperty,
isService
} from '../common/decorators';
import {
CategorizedClassDoc,
CategorizedClassLikeDoc,
CategorizedMethodMemberDoc,
CategorizedClassDoc, CategorizedClassLikeDoc, CategorizedMethodMemberDoc,
CategorizedPropertyMemberDoc
} from '../common/dgeni-definitions';
import {normalizeMethodParameters} from '../common/normalize-method-parameters';
import {getInputBindingData, getOutputBindingData} from '../common/property-bindings';
import {sortCategorizedMembers} from '../common/sort-members';


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

// Special decorations for real class documents that don't apply for interfaces.
if (classLikeDoc.docType === 'class') {
this.decorateClassDoc(classLikeDoc as CategorizedClassDoc);
}

// Call decorate hooks that can modify the method and property docs.
classLikeDoc.methods.forEach(doc => this.decorateMethodDoc(doc));
classLikeDoc.properties.forEach(doc => this.decoratePropertyDoc(doc));
Expand All @@ -65,11 +60,6 @@ export class Categorizer implements Processor {
// Sort members
classLikeDoc.methods.sort(sortCategorizedMembers);
classLikeDoc.properties.sort(sortCategorizedMembers);

// Special decorations for real class documents that don't apply for interfaces.
if (classLikeDoc.docType === 'class') {
this.decorateClassDoc(classLikeDoc as CategorizedClassDoc);
}
}

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

// Categorize the current visited classDoc into its Angular type.
if (isDirective(classDoc)) {
if (isDirective(classDoc) && classDoc.directiveMetadata) {
classDoc.isDirective = true;
classDoc.directiveExportAs = getMetadataProperty(classDoc, 'exportAs');
classDoc.directiveExportAs = classDoc.directiveMetadata.get('exportAs');
classDoc.directiveSelectors = getDirectiveSelectors(classDoc);
} else if (isService(classDoc)) {
classDoc.isService = true;
Expand Down Expand Up @@ -114,13 +105,17 @@ export class Categorizer implements Processor {
private decoratePropertyDoc(propertyDoc: CategorizedPropertyMemberDoc) {
decorateDeprecatedDoc(propertyDoc);

// TODO(devversion): detect inputs based on the `inputs` property in the component metadata.
const metadata = propertyDoc.containerDoc.docType === 'class' ?
(propertyDoc.containerDoc as CategorizedClassDoc).directiveMetadata : null;

const inputMetadata = metadata ? getInputBindingData(propertyDoc, metadata) : null;
const outputMetadata = metadata ? getOutputBindingData(propertyDoc, metadata) : null;

propertyDoc.isDirectiveInput = isDirectiveInput(propertyDoc);
propertyDoc.directiveInputAlias = getDirectiveInputAlias(propertyDoc);
propertyDoc.isDirectiveInput = !!inputMetadata;
propertyDoc.directiveInputAlias = (inputMetadata && inputMetadata.alias) || '';

propertyDoc.isDirectiveOutput = isDirectiveOutput(propertyDoc);
propertyDoc.directiveOutputAlias = getDirectiveOutputAlias(propertyDoc);
propertyDoc.isDirectiveOutput = !!outputMetadata;
propertyDoc.directiveOutputAlias = (outputMetadata && outputMetadata.alias) || '';
}
}

Expand Down
9 changes: 8 additions & 1 deletion tools/dgeni/processors/merge-inherited-properties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,14 @@ export class MergeInheritedProperties implements Processor {

private addMemberDocIfNotPresent(destination: ClassExportDoc, memberDoc: MemberDoc) {
if (!destination.members.find(member => member.name === memberDoc.name)) {
destination.members.push(memberDoc);
// To be able to differentiate between member docs from the heritage clause and the
// member doc for the destination class, we clone the member doc. It's important to keep
// the prototype and reference because later, Dgeni identifies members and properties
// by using an instance comparison.
const newMemberDoc = Object.assign(Object.create(memberDoc), memberDoc);
newMemberDoc.containerDoc = destination;

destination.members.push(newMemberDoc);
}
}
}