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
This commit is contained in:
Paul Gschwendtner
2018-01-24 01:08:11 +01:00
committed by Jeremy Elbourn
parent c16a36057e
commit e0fc2bd51c
7 changed files with 166 additions and 61 deletions
+6 -29
View File
@@ -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]',
@@ -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 getDirectiveSelectors(classDoc: CategorizedClassDoc) {
if (!classDoc.directiveMetadata) {
return;
}
export function getDirectiveOutputAlias(doc: PropertyMemberDoc) {
return isDirectiveOutput(doc) ? doc.decorators!.find(d => d.name == 'Output')!.arguments![0] : '';
}
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.
@@ -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);
}
+1
View File
@@ -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;
}
+73
View File
@@ -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
View File
@@ -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]
};
}
}
+4 -5
View File
@@ -1,4 +1,3 @@
import {isDirectiveInput, isDirectiveOutput} from './decorators';
import {CategorizedMethodMemberDoc, CategorizedPropertyMemberDoc} from './dgeni-definitions';
/** Combined type for a categorized method member document. */
@@ -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;
}
+21 -26
View File
@@ -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';
@@ -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));
@@ -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);
}
}
/**
@@ -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;
@@ -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;
propertyDoc.isDirectiveInput = isDirectiveInput(propertyDoc);
propertyDoc.directiveInputAlias = getDirectiveInputAlias(propertyDoc);
const inputMetadata = metadata ? getInputBindingData(propertyDoc, metadata) : null;
const outputMetadata = metadata ? getOutputBindingData(propertyDoc, metadata) : null;
propertyDoc.isDirectiveOutput = isDirectiveOutput(propertyDoc);
propertyDoc.directiveOutputAlias = getDirectiveOutputAlias(propertyDoc);
propertyDoc.isDirectiveInput = !!inputMetadata;
propertyDoc.directiveInputAlias = (inputMetadata && inputMetadata.alias) || '';
propertyDoc.isDirectiveOutput = !!outputMetadata;
propertyDoc.directiveOutputAlias = (outputMetadata && outputMetadata.alias) || '';
}
}
@@ -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);
}
}
}