mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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:
committed by
Jeremy Elbourn
parent
c16a36057e
commit
e0fc2bd51c
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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]
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user