949 lines
34 KiB
TypeScript
949 lines
34 KiB
TypeScript
import { ClassDeclaration, Decorator, ExportedDeclarations, FunctionDeclaration, InterfaceDeclaration, MethodDeclaration, Node, PropertyDeclaration, SourceFile, Type } from "ts-morph";
|
|
|
|
export type TypeInformation = {
|
|
isBaseType: boolean;
|
|
baseType: string;
|
|
simplifiedType?: string;
|
|
simplifiedSubType?: string;
|
|
typeImports?: {
|
|
path: string;
|
|
identifier: string;
|
|
}[];
|
|
originalCode: string;
|
|
}
|
|
|
|
export type ModifierInformation = {
|
|
declaration: MethodDeclaration | PropertyDeclaration,
|
|
modifiers: string[],
|
|
isPublic: boolean,
|
|
isPrivate: boolean,
|
|
isProtected: boolean,
|
|
isReadonly: boolean,
|
|
}
|
|
|
|
export type ParameterInformation = ({
|
|
name: string;
|
|
originalCode: string;
|
|
index: number;
|
|
authorDescription: string;
|
|
isOptional: boolean;
|
|
} & TypeInformation);
|
|
|
|
export type MethodInformation = {
|
|
declaration: MethodDeclaration;
|
|
params: ParameterInformation[];
|
|
isAbstract: boolean;
|
|
name: string;
|
|
isAsync: boolean;
|
|
isGenerator: boolean;
|
|
isImplementation: boolean;
|
|
returnType: TypeInformation;
|
|
head: string;
|
|
authorDescription: string;
|
|
}
|
|
|
|
export type DecoratorInformation = {
|
|
declaration: MethodDeclaration | PropertyDeclaration | ClassDeclaration,
|
|
decoratorNames: string[],
|
|
decorators: Decorator[],
|
|
decoratorSettings: { [index: string]: { [index: string]: any } }
|
|
}
|
|
|
|
export type ImportMapping = {
|
|
mapping: {
|
|
[index: string]: {
|
|
importSrc: string;
|
|
alias?: string;
|
|
};
|
|
};
|
|
aliasToOriginal: {
|
|
[index: string]: string;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Interface for the Analyzing Result
|
|
*/
|
|
export interface IAnalyzeResult {
|
|
// Name of the Class
|
|
className: string,
|
|
// Decorators of the Class
|
|
classDecorator: DecoratorInformation,
|
|
// Methods of the Class
|
|
methods: (MethodInformation & DecoratorInformation)[],
|
|
// Properties of the Class
|
|
properties: (PropertyInformation & DecoratorInformation)[],
|
|
// Imports of the Class (contians external Files)
|
|
imports: {
|
|
content: string,
|
|
required: boolean,
|
|
}
|
|
}
|
|
|
|
export type PropertyInformation = ModifierInformation & TypeInformation & { name: string; declaration: PropertyDeclaration; };
|
|
|
|
/**
|
|
* Helperfunction, used to extract a Mapping of imported Files. This allows the user to rename the interfaces
|
|
* to custom names. this will then be mapped to the original name
|
|
* @param file The Source File.
|
|
*/
|
|
export function getImportsOfFile(file: SourceFile) {
|
|
|
|
const mapping: {
|
|
[index: string]: {
|
|
// The Source that is used for importing the class.
|
|
importSrc: string,
|
|
|
|
// an alias for the classs
|
|
alias?: string,
|
|
|
|
}
|
|
} = {};
|
|
|
|
// Maps alias to original names
|
|
const aliasToOriginal: { [index: string]: string } = {};
|
|
|
|
// Filter the Imports, that only use named imports.
|
|
file.getImportDeclarations().filter(_import => _import.getNamedImports().length > 0).map(_import => {
|
|
// Extract the Name and loaded Module / Path.
|
|
return {
|
|
namespaces: _import.getNamedImports().map(_namedImport => {
|
|
// Store the Import with the File.
|
|
mapping[_namedImport.getName()] = {
|
|
importSrc: _import.getModuleSpecifierValue(),
|
|
};
|
|
// If an alias is used update the import.
|
|
if (_namedImport.getAliasNode()) {
|
|
mapping[_namedImport.getName()] = {
|
|
importSrc: _import.getModuleSpecifierValue(),
|
|
alias: _namedImport.getAliasNode().getText()
|
|
};
|
|
aliasToOriginal[_namedImport.getAliasNode().getText()] = _namedImport.getName();
|
|
}
|
|
return _namedImport.getName();
|
|
}),
|
|
module: _import.getModuleSpecifierValue()
|
|
}
|
|
});
|
|
|
|
return { mapping, aliasToOriginal };
|
|
}
|
|
|
|
/**
|
|
* Function to test if a class (cl) implements a specific interface named ifName.
|
|
* @param cl The Class
|
|
* @param ifName The Name of the Interface.
|
|
* @param aliasToOriginal Mapping of aliases to original imported names.
|
|
* @param caseSensitive Flag to toggle on / off the case sensitivity
|
|
*/
|
|
export function isClassImplementingInterface(cl: ClassDeclaration, ifName: string, aliasToOriginal: { [index: string]: string }, caseSensitive = true) {
|
|
|
|
// Get the Implemented Interfaces of the Class.
|
|
let interfaces = getImplementedInterface(cl, aliasToOriginal);
|
|
|
|
// If the CaseSensitive is turned change all names to lowercase.
|
|
// although the requeste name
|
|
if (!caseSensitive) {
|
|
interfaces = interfaces.map(iface => iface.toLocaleLowerCase());
|
|
ifName = ifName.toLowerCase();
|
|
}
|
|
|
|
// Return the test, whether the requested Interface name is
|
|
// included in the list of interfaces.
|
|
return interfaces.includes(ifName);
|
|
}
|
|
|
|
/**
|
|
* Function to list all implemented interfaces
|
|
* @param cl the class descriptor
|
|
* @param aliasToOriginal Mapping of aliases to original imported names.
|
|
*/
|
|
export function getImplementedInterface(cl: ClassDeclaration, aliasToOriginal: { [index: string]: string }) {
|
|
|
|
// Iterate over the implemented Interface and
|
|
// extract the interface names. If an alias for
|
|
// an interface is used => use the original name
|
|
// of the Interface => interfaces contains only
|
|
// the "original names" of the implemented interfaces
|
|
let interfaces = cl.getImplements().map(iface => {
|
|
|
|
const name = iface.getText();
|
|
|
|
if (typeof aliasToOriginal[name] === 'string') {
|
|
return aliasToOriginal[name]
|
|
}
|
|
|
|
return name;
|
|
});
|
|
|
|
// return the list with implemented interfaces.
|
|
return interfaces;
|
|
}
|
|
|
|
/**
|
|
* Function, used to analyze the Type of a Prop / Parameter etc.
|
|
* @param node The node used to describe the element
|
|
* @param inputType The type
|
|
* @param text The textual representation.
|
|
*/
|
|
function _getType(node: Node, inputType: Type, text: string) {
|
|
|
|
// Define return Properties.
|
|
let baseType = '';
|
|
let simplifiedType = '';
|
|
let simplifiedSubType = '';
|
|
let typeImports: { path: string, identifier: string }[] = [];
|
|
let originalCode = inputType.getText();
|
|
|
|
// Test if the Type is a Base-Type.
|
|
if ((inputType.compilerType as any).intrinsicName) {
|
|
// Basic Type.
|
|
baseType = (inputType.compilerType as any).intrinsicName;
|
|
|
|
} else if (inputType.compilerType.symbol) {
|
|
|
|
// A complex Type like a Map etc.
|
|
baseType = inputType.compilerType.symbol.escapedName.toString();
|
|
|
|
// Regex to extrat the Imports with the corresponding type.
|
|
const externalTypes = /import\(.+?\)\.\w+/g;
|
|
const result = [...text.matchAll(externalTypes)];
|
|
|
|
// Use the Regex to remove the Imports
|
|
const regex = /import\(.+?\)./g
|
|
simplifiedType = text.replace(regex, '');
|
|
|
|
// Only if the type isnt a function the result will be present:
|
|
if (result.length > 0) {
|
|
|
|
// Update the Imported Types.
|
|
typeImports.push(...result.map(item => {
|
|
const text = item.toString();
|
|
|
|
// Regex, to extract the path of the Import.
|
|
const regex = /(?<=")(.*)(?=")/g;
|
|
const path = regex.exec(text)[0].toString();
|
|
// Get the corresponding Typ-Identifier
|
|
const identifier = text.split(').')[1];
|
|
|
|
return {
|
|
path,
|
|
identifier
|
|
};
|
|
}));
|
|
|
|
} else if (Node.isFunctionDeclaration(node) || Node.isFunctionTypeNode(node) || Node.isFunctionLikeDeclaration(node)) {
|
|
baseType = "function";
|
|
|
|
// Iterate over the parameter
|
|
node.getParameters().map(parameter => {
|
|
const defintion = _getType(
|
|
parameter.getTypeNode(),
|
|
parameter.getType(),
|
|
parameter.getType().getText()
|
|
)
|
|
|
|
if (!defintion.isBaseType) {
|
|
// Add the Elements to the Import.
|
|
typeImports.push(...defintion.typeImports)
|
|
}
|
|
})
|
|
}
|
|
|
|
// Define the a Simplified Subtype.
|
|
simplifiedSubType = simplifiedType
|
|
if (simplifiedType.includes(baseType + '<') && simplifiedType[simplifiedType.length - 1] === '>') {
|
|
simplifiedSubType = simplifiedType.slice(baseType.length + '<'.length, simplifiedType.length - 1);
|
|
}
|
|
}
|
|
|
|
// Define a Partial element of the Return-Value.
|
|
const ret: TypeInformation = {
|
|
isBaseType: !!(inputType.compilerType as any).intrinsicName,
|
|
baseType,
|
|
originalCode,
|
|
typeImports
|
|
}
|
|
|
|
// Add the Additional elements if required.
|
|
if (!ret.isBaseType) {
|
|
ret.simplifiedType = simplifiedType;
|
|
ret.simplifiedSubType = simplifiedSubType;
|
|
ret.typeImports = typeImports;
|
|
}
|
|
|
|
// Return the Type.
|
|
return ret;
|
|
|
|
}
|
|
|
|
/**
|
|
* Function to test, whether a Function is defined as a specific type.
|
|
* @param prop The Property Declaration
|
|
* @param reqType The requested Type, that should be matched
|
|
* @param caseSensitive A Flage to use casesesitivy during the checkoup
|
|
*/
|
|
export function isPropOfType(prop: PropertyDeclaration, reqType: string, caseSensitive = true) {
|
|
if (!caseSensitive) {
|
|
return reqType.toLowerCase() === (getDescription(prop) as PropertyInformation).baseType.toLowerCase();
|
|
} else {
|
|
return reqType === (getDescription(prop) as PropertyInformation).baseType;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Function get a Description of different Files.
|
|
* @param declaration The Declaration to analyze
|
|
*/
|
|
export function getDescription(declaration: PropertyDeclaration | InterfaceDeclaration | FunctionDeclaration | MethodDeclaration | ExportedDeclarations): PropertyInformation | MethodInformation | TypeInformation {
|
|
|
|
let typeInformation: TypeInformation;
|
|
|
|
if (Node.isPropertyDeclaration(declaration)) {
|
|
typeInformation = _getType(
|
|
declaration.getTypeNode(),
|
|
declaration.getType(),
|
|
declaration.getType().getText()
|
|
);
|
|
|
|
return Object.assign(
|
|
// Use the Modifiers
|
|
getModifiers(declaration),
|
|
// Provide the Type Information.
|
|
Object.assign(
|
|
// the Type Description
|
|
typeInformation,
|
|
{
|
|
// And use the Propertiy Name.
|
|
name: declaration.getName(),
|
|
// Keep the declartion object as well
|
|
declaration
|
|
}
|
|
)
|
|
);
|
|
} else if (Node.isInterfaceDeclaration(declaration)) {
|
|
|
|
const typeImports: {
|
|
path: string,
|
|
identifier: string
|
|
}[] = [];
|
|
|
|
declaration.getProperties().map(p => {
|
|
let text = p.getType().getText();
|
|
const externalTypesRegex = /import\(.+?\)\.\w+/g;
|
|
const externalTypes = [...text.matchAll(externalTypesRegex)];
|
|
|
|
for (const externalType of externalTypes) {
|
|
const section = externalType.toString();
|
|
|
|
const regexType = /import\(.+?\)./g;
|
|
const identifier = section.replace(regexType, '');
|
|
|
|
const regexPath = /(?<=import\(").+?(?="\).)/g;
|
|
const path = [...section.matchAll(regexPath)][0].toString();
|
|
|
|
typeImports.push({
|
|
path,
|
|
identifier
|
|
})
|
|
}
|
|
});
|
|
|
|
// // Define a Partial element of the Return-Value.
|
|
const ret: TypeInformation = {
|
|
isBaseType: false,
|
|
baseType: declaration.getName(),
|
|
originalCode: declaration.getText(),
|
|
typeImports,
|
|
}
|
|
|
|
// Return the Type.
|
|
return ret;
|
|
|
|
} else if (Node.isParameterDeclaration(declaration)) {
|
|
//
|
|
} else if (Node.isMethodDeclaration(declaration)) {
|
|
const isAbstract = declaration.isAbstract();
|
|
const name = declaration.getName();
|
|
const isGenerator = declaration.isGenerator();
|
|
const isImplementation = declaration.isImplementation();
|
|
const retTypeObj = declaration.getReturnType();
|
|
const returnType = _getType(
|
|
declaration.getReturnTypeNode(),
|
|
retTypeObj,
|
|
retTypeObj.getText()
|
|
);
|
|
|
|
// Use the Regex to remove the Imports from the Head
|
|
const regex = /import\(.+?\)./g
|
|
// Define the Head
|
|
let head = declaration.getType().getText().replace(regex, '');
|
|
head = head.slice(0, head.length - ('=> ' + returnType.simplifiedType).length)
|
|
|
|
// Extract the Description of the Author.
|
|
const authorDescription = declaration.getLeadingCommentRanges().map(comment => comment.getText()).join('\n');
|
|
// Flag if the Function is Performed Async. (this is achieved by retruning a promise or adding an async tag in the beginning)
|
|
const isAsync = declaration.isAsync() || returnType.baseType == 'Promise';
|
|
|
|
// Extract the Parameters of the Function
|
|
const params: ParameterInformation[] = declaration.getParameters().map((parameter, index) => {
|
|
return Object.assign(
|
|
{
|
|
// Name of the parameter
|
|
name: parameter.getName(),
|
|
// The Originale Code
|
|
originalCode: parameter.getText(),
|
|
// The Index of the Parameter
|
|
index,
|
|
// Extract the Information provided for the Parameter (If available.)
|
|
authorDescription: (parameter.getLeadingCommentRanges() || parameter.getTrailingCommentRanges()).map(comment => comment.getText()).join('\n'),
|
|
// Extract whether the Parameters is Optional or not.
|
|
isOptional: parameter.isOptional()
|
|
},
|
|
_getType(
|
|
parameter.getTypeNode(),
|
|
parameter.getType(),
|
|
parameter.getText()
|
|
)
|
|
)
|
|
});
|
|
|
|
return {
|
|
declaration,
|
|
params,
|
|
isAbstract,
|
|
head,
|
|
name,
|
|
isAsync,
|
|
isGenerator,
|
|
isImplementation,
|
|
returnType,
|
|
authorDescription
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Function to extract the Matching Properts
|
|
* @param cl The Class
|
|
* @param reqType The requested Type of the Element
|
|
* @param caseSensitive A Flage to use casesesitivy during the checkoup
|
|
*/
|
|
export function getMatchingProperties(cl: ClassDeclaration): PropertyInformation[] {
|
|
return cl.getProperties()
|
|
// Instead of returning the Property Declaration, return the
|
|
// Property Descriptor.
|
|
.map(
|
|
propertyDeclaration => getDescription(propertyDeclaration) as PropertyInformation
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Function to extract the Decorator settings of the defined type.
|
|
* @param declaration The declaration to test.
|
|
* @param decorator The decorator, that should be used.
|
|
* @param aliasToOriginal Mapping of aliases to original imported names.
|
|
* @param caseSensitive Turn off / on case sensitive for the checked decorator
|
|
* @param extractArgs Turn off / on extracting the Arguments of the Decorator.
|
|
*/
|
|
export function getDecorators(declaration: MethodDeclaration | PropertyDeclaration | ClassDeclaration, decorator: string, aliasToOriginal: { [index: string]: string }, caseSensitive = true, extractArgs = true) {
|
|
|
|
// Define the Returntype.
|
|
const ret: DecoratorInformation = {
|
|
declaration,
|
|
decoratorNames: [],
|
|
decoratorSettings: {},
|
|
decorators: []
|
|
}
|
|
|
|
if (!caseSensitive) {
|
|
decorator = decorator.toLowerCase();
|
|
}
|
|
|
|
ret.decorators = declaration.getDecorators()
|
|
.filter(usedDecorator => {
|
|
// Get the Name of the Decorator
|
|
let nameOfDecorator = usedDecorator.getName();
|
|
|
|
// Let CaseSensitive or not
|
|
if (!caseSensitive) {
|
|
nameOfDecorator = nameOfDecorator.toLowerCase();
|
|
}
|
|
|
|
if (extractArgs) {
|
|
// Try to extract the arguments of the Decorator (doest work on none static object / elements)
|
|
const _arguments = usedDecorator.getArguments();
|
|
|
|
if (_arguments.length > 0) {
|
|
_arguments.map(a => {
|
|
// Parse the Text
|
|
const text = a.getText();
|
|
|
|
// Bad Practice. Create the Code create Function that will create the Object.
|
|
try {
|
|
ret.decoratorSettings[nameOfDecorator] = eval('() => { return ' + text + '}')();
|
|
} catch (e) {
|
|
// Failed to Parse
|
|
}
|
|
});
|
|
} else {
|
|
// ret.decoratorSettings[nameOfDecorator] = null
|
|
}
|
|
}
|
|
|
|
// If the Decorator uses an alias => return
|
|
// the test with the original name of the decorator.
|
|
if (typeof aliasToOriginal[nameOfDecorator] === 'string') {
|
|
return decorator === aliasToOriginal[nameOfDecorator]
|
|
}
|
|
|
|
return decorator === nameOfDecorator;
|
|
});
|
|
|
|
ret.decoratorNames = ret.decorators.map(d => d.getName())
|
|
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* Function to test if the Property is injected or not.
|
|
* @param prop The Property Declaration
|
|
* @param decorator The decorator, that should be used.
|
|
* @param aliasToOriginal Mapping of aliases to original imported names.
|
|
* @param caseSensitive Turn off / on case sensitive for the checked decorator
|
|
*/
|
|
export function isPropertyInjectedWith(prop: PropertyDeclaration, decorator: string, aliasToOriginal: { [index: string]: string }, caseSensitive = true) {
|
|
|
|
if (!caseSensitive) {
|
|
decorator = decorator.toLowerCase();
|
|
}
|
|
|
|
let decorators = getDecorators(prop, decorator, aliasToOriginal, caseSensitive, false);
|
|
return decorators.decorators.length > 0;
|
|
}
|
|
|
|
/**
|
|
* Function to extrac the Modifiers of a declaration.
|
|
* @param declaration The Declartion of the Class
|
|
*/
|
|
export function getModifiers(declaration: MethodDeclaration | PropertyDeclaration) {
|
|
|
|
// Dictionary used to match the Keywords
|
|
const dict = {
|
|
PublicKeyword: 'public',
|
|
ProtectedKeyword: 'protected',
|
|
PrivateKeyword: 'private',
|
|
0: ['public'],
|
|
4: ['public'],
|
|
8: ['private'],
|
|
16: ['protected'],
|
|
68: ['public', 'readonly'],
|
|
72: ['private', 'readonly'],
|
|
80: ['protected', 'readonly'],
|
|
}
|
|
|
|
let modifiers: string[] = [];
|
|
|
|
// Handle Methods
|
|
if ((declaration as MethodDeclaration).getOverloads) {
|
|
modifiers = (declaration as MethodDeclaration).getOverloads().map(overload => dict[overload.getName()]);
|
|
|
|
// Handle Properties
|
|
} else if ((declaration as PropertyDeclaration).getCombinedModifierFlags) {
|
|
modifiers = dict[(declaration as PropertyDeclaration).getCombinedModifierFlags()];
|
|
}
|
|
|
|
// If nothing is provided => Defaults to public
|
|
if (modifiers.length === 0) {
|
|
modifiers.push('public')
|
|
}
|
|
|
|
const ret: ModifierInformation = {
|
|
declaration,
|
|
modifiers,
|
|
isPublic: modifiers.includes('public'),
|
|
isPrivate: modifiers.includes('private'),
|
|
isProtected: modifiers.includes('protected'),
|
|
isReadonly: modifiers.includes('readonly'),
|
|
};
|
|
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* Method to create a Default Filter for Methods.
|
|
* @param options
|
|
*/
|
|
export function defaultClassFilter(options: {
|
|
classDecorator: string,
|
|
classInterface: string,
|
|
}) {
|
|
return (cl: ClassDeclaration, importMapping: ImportMapping) => {
|
|
return (
|
|
options.classInterface === '' ||
|
|
isClassImplementingInterface(cl, options.classInterface, importMapping.aliasToOriginal)
|
|
) && (
|
|
options.classDecorator === '' ||
|
|
getDecorators(cl, options.classDecorator, importMapping.aliasToOriginal)
|
|
) as boolean
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Default Method for filtering Methods.
|
|
*/
|
|
export function defaultMethodFilter() {
|
|
return (cl: ClassDeclaration, method: (MethodInformation & DecoratorInformation & ModifierInformation), importMapping: ImportMapping) => {
|
|
return method.isPublic && method.isAsync;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Default Filter, to Filter Properties
|
|
* @param options The Options to use.
|
|
*/
|
|
export function defaultPropFilter(options: {
|
|
propertyType: string,
|
|
propertyDecorator: string,
|
|
}) {
|
|
return (cl: ClassDeclaration, property: (PropertyInformation & DecoratorInformation & ModifierInformation), importMapping: ImportMapping) => {
|
|
return isPropOfType(property.declaration, options.propertyType, false) &&
|
|
property.isPublic &&
|
|
property.decoratorNames.includes(options.propertyDecorator)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper Function to List relevant classes with their corresponding elements
|
|
* @param sources The Source Files
|
|
* @param classDecorator Filter for the Class Decorators
|
|
* @param classInterface Interfaces that should be implemented by the class
|
|
* @param methodDecorator A Method-Decorator
|
|
* @param propertyType The requrired Type for the Property
|
|
* @param propertyDecorator The Decorator for the Property
|
|
*/
|
|
export function analyzeClasses(sources: SourceFile[], options: {
|
|
filterClasses: (cl: ClassDeclaration, importMapping: ImportMapping) => boolean,
|
|
filterMethods: (cl: ClassDeclaration, method: (MethodInformation & DecoratorInformation & ModifierInformation), importMapping: ImportMapping) => boolean,
|
|
filterProperties: (cl: ClassDeclaration, property: (PropertyInformation & DecoratorInformation & ModifierInformation), importMapping: ImportMapping) => boolean,
|
|
classDecorator: string,
|
|
methodDecorator: string,
|
|
propertyDecorator: string,
|
|
}) {
|
|
|
|
const ret: {
|
|
className: string;
|
|
decorator: DecoratorInformation,
|
|
methods: (MethodInformation & DecoratorInformation & ModifierInformation)[];
|
|
properties: (PropertyInformation & DecoratorInformation & ModifierInformation)[];
|
|
}[] = [];
|
|
|
|
// Iterate over the Files:
|
|
for (const file of sources) {
|
|
// For Each File => Analyze the imported Files.
|
|
// Create a Mapping File for the Improts.
|
|
const importMapping = getImportsOfFile(file);
|
|
|
|
// After all Imports has been detected => filter for all Classes that implement the provided classDecorator
|
|
const relevantClasses = file.getClasses().filter(cl => options.filterClasses(cl, importMapping));
|
|
|
|
// Now after each class is known => ierate over the relevant classes
|
|
// and get their relevant Methods and Attributes.
|
|
for (const relevantClass of relevantClasses) {
|
|
// Extract the Methods.
|
|
const sharedMethodsOfClass = relevantClass.getMethods()
|
|
.map(method => getDecorators(method, options.methodDecorator, importMapping.aliasToOriginal))
|
|
.filter(methodObject => methodObject.decorators.length > 0);
|
|
|
|
// Parsed Method
|
|
const parsedMethods = sharedMethodsOfClass
|
|
.map(methodObject =>
|
|
Object.assign(
|
|
getDescription(methodObject.declaration as MethodDeclaration) as MethodInformation,
|
|
getDecorators(
|
|
methodObject.declaration,
|
|
options.methodDecorator,
|
|
importMapping.aliasToOriginal
|
|
),
|
|
getModifiers(methodObject.declaration as MethodDeclaration)
|
|
)
|
|
).filter(item => options.filterMethods(
|
|
relevantClass,
|
|
item,
|
|
importMapping
|
|
));
|
|
|
|
// Get the Properties
|
|
const relevantProperties = getMatchingProperties(relevantClass)
|
|
.map(property =>
|
|
Object.assign(
|
|
property,
|
|
getDecorators(
|
|
property.declaration,
|
|
options.propertyDecorator,
|
|
importMapping.aliasToOriginal,
|
|
false,
|
|
true
|
|
),
|
|
getModifiers(property.declaration)
|
|
)
|
|
).filter(property => options.filterProperties(relevantClass, property, importMapping)
|
|
)
|
|
|
|
const item = {
|
|
decorator: getDecorators(
|
|
relevantClass,
|
|
options.classDecorator,
|
|
importMapping.aliasToOriginal,
|
|
),
|
|
className: relevantClass.getName(),
|
|
methods: parsedMethods,
|
|
properties: relevantProperties
|
|
};
|
|
|
|
ret.push(item)
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* Create a Mapping of Files. for simpler access.
|
|
* @param files
|
|
*/
|
|
export function createFileMapping(files: SourceFile[]) {
|
|
// Define the Return type.
|
|
const ret: {
|
|
[index: string]: SourceFile
|
|
} = {};
|
|
|
|
for (const file of files) {
|
|
ret[file.getFilePath()] = file;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* Function to extrac the Declaration by the Name of the Element
|
|
* @param files A File Mapping
|
|
* @param filePath The Relevant File
|
|
* @param identifier The Identifier of the Element, on which the Description should be extracted
|
|
* @param types Internal Object, used for recursion.
|
|
*/
|
|
export function getDeclarationByName(files: { [index: string]: SourceFile }, filePath: string, identifier: string, types: { [index: string]: TypeInformation } = {}) {
|
|
|
|
// Make shur the File is named correctly.
|
|
if (!filePath.endsWith('.ts')) {
|
|
filePath = filePath + '.ts';
|
|
}
|
|
|
|
// Iterate over all files, to finde the correct one.
|
|
const file = files[filePath];
|
|
if (file) {
|
|
// Get all Declarations
|
|
const declarations = file.getExportedDeclarations();
|
|
// Get the Imports of the File.
|
|
const importMapping = getImportsOfFile(file);
|
|
|
|
// If the idenfifiert is know, go on otherwise throw an Error.
|
|
if (declarations.has(identifier)) {
|
|
const exportedDeclarations = declarations.get(identifier);
|
|
const typesToTest = exportedDeclarations.map(e => getDescription(e) as PropertyInformation | TypeInformation);
|
|
const recursiveTest = new Array<{
|
|
path: string, identifier: string
|
|
}>();
|
|
|
|
// Iterate as long, as there are elements to test
|
|
while (typesToTest.length > 0) {
|
|
// Get the Type.
|
|
const type = typesToTest.pop();
|
|
|
|
// Flag, indicating, whether the element has been imported or not
|
|
const isImported = importMapping.aliasToOriginal[type.baseType] !== undefined || importMapping.mapping[type.baseType] !== undefined;
|
|
// Flag, showing whether the element imports other Items or not.
|
|
const hasImport = type.typeImports && type.typeImports.length > 0;
|
|
|
|
// If the Element isnt imported => simply add the item Type
|
|
if (!isImported && types[type.originalCode] === undefined) {
|
|
types[type.originalCode] = type;
|
|
} else if (isImported && types[type.originalCode] === undefined) {
|
|
recursiveTest.push({
|
|
path: importMapping.mapping[type.baseType].importSrc,
|
|
identifier: type.baseType
|
|
})
|
|
}
|
|
|
|
if (hasImport) {
|
|
type.typeImports.map(({ path, identifier }) => {
|
|
// Push the Elements to the Reverse Test.
|
|
recursiveTest.push({
|
|
path,
|
|
identifier
|
|
})
|
|
});
|
|
}
|
|
}
|
|
|
|
// Call this Function recursively.
|
|
for (const rec of recursiveTest) {
|
|
getDeclarationByName(files, rec.path, rec.identifier, types)
|
|
}
|
|
|
|
return Object.getOwnPropertyNames(types).map(key => types[key]);
|
|
}
|
|
|
|
}
|
|
|
|
throw Error('Declaration "' + filePath + '" not found');
|
|
}
|
|
|
|
/**
|
|
* Function, that will create the imported Type classes based on source-Files.
|
|
* @param sourceFiles
|
|
* @param options Options, to Controll the generation.
|
|
*/
|
|
export function analyzeFiles(sourceFiles: SourceFile[], options: {
|
|
filterClasses: (cl: ClassDeclaration, importMapping: ImportMapping) => boolean,
|
|
filterMethods: (cl: ClassDeclaration, method: (MethodInformation & DecoratorInformation & ModifierInformation), importMapping: ImportMapping) => boolean,
|
|
filterProperties: (cl: ClassDeclaration, property: (PropertyInformation & DecoratorInformation & ModifierInformation), importMapping: ImportMapping) => boolean,
|
|
checkImport: (type: string) => boolean,
|
|
classDecorator: string,
|
|
methodDecorator: string,
|
|
propertyDecorator: string,
|
|
} = {
|
|
classDecorator: 'exportsElementsToDispatcher',
|
|
methodDecorator: 'exportMethodToDispatcher',
|
|
propertyDecorator: 'exportPropertyToDispatcher',
|
|
filterClasses: defaultClassFilter({
|
|
classDecorator: 'exportsElementsToDispatcher',
|
|
classInterface: ''
|
|
}),
|
|
filterMethods: defaultMethodFilter(),
|
|
filterProperties: defaultPropFilter({
|
|
propertyDecorator: 'exportPropertyToDispatcher',
|
|
propertyType: 'nopeObservable'
|
|
}),
|
|
checkImport: type => type !== 'nopeObservable'
|
|
}) {
|
|
const fileMapping = createFileMapping(sourceFiles);
|
|
const classes = analyzeClasses(sourceFiles, options);
|
|
|
|
const ret: IAnalyzeResult[] = []
|
|
|
|
// Iterate over the Classes
|
|
for (const relevantClass of classes) {
|
|
|
|
const item: IAnalyzeResult = {
|
|
className: relevantClass.className,
|
|
classDecorator: relevantClass.decorator,
|
|
methods: [],
|
|
properties: [],
|
|
imports: {
|
|
content: '',
|
|
required: false
|
|
}
|
|
}
|
|
|
|
const requiredImports = new Set<string>();
|
|
const mappingTypeToImport: {
|
|
[index: string]: Set<string>
|
|
} = {}
|
|
|
|
// Iterate over the Properties
|
|
for (const prop of relevantClass.properties) {
|
|
if (!prop.isBaseType) {
|
|
for (const { identifier, path } of (prop.typeImports || [])) {
|
|
|
|
// Only if the import isnt the Base Type, add it to the List.
|
|
if (options.checkImport(identifier)) {
|
|
// Check if the Imported Type has been Adden multiple Times.
|
|
if (mappingTypeToImport[identifier] === undefined) {
|
|
mappingTypeToImport[identifier] = new Set<string>();
|
|
}
|
|
|
|
mappingTypeToImport[identifier].add(path);
|
|
requiredImports.add(identifier);
|
|
}
|
|
}
|
|
}
|
|
|
|
item.properties.push(prop)
|
|
}
|
|
|
|
// Iterate over the Methods
|
|
for (const method of relevantClass.methods) {
|
|
if (!method.returnType.isBaseType) {
|
|
for (const { identifier, path } of (method.returnType.typeImports || [])) {
|
|
|
|
// Only if the import isnt the Base Type, add it to the List.
|
|
if (options.checkImport(identifier)) {
|
|
// Check if the Imported Type has been Adden multiple Times.
|
|
if (mappingTypeToImport[identifier] === undefined) {
|
|
mappingTypeToImport[identifier] = new Set<string>();
|
|
}
|
|
|
|
mappingTypeToImport[identifier].add(path);
|
|
requiredImports.add(identifier);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Iterate over the Parameters and extract
|
|
// the required elements. (If they arent base
|
|
// types).
|
|
for (const parm of method.params) {
|
|
if (!parm.isBaseType) {
|
|
for (const { identifier, path } of (parm.typeImports || [])) {
|
|
|
|
// Only if the import isnt the Base Type, add it to the List.
|
|
if (options.checkImport(identifier)) {
|
|
// Check if the Imported Type has been Adden multiple Times.
|
|
if (mappingTypeToImport[identifier] === undefined) {
|
|
mappingTypeToImport[identifier] = new Set<string>();
|
|
}
|
|
|
|
mappingTypeToImport[identifier].add(path);
|
|
requiredImports.add(identifier);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
item.methods.push(method)
|
|
}
|
|
|
|
item.imports.content = '';
|
|
item.imports.required = requiredImports.size > 0;
|
|
|
|
// List containing the already used Types
|
|
const importedBaseTypes = new Set<string>();
|
|
|
|
// Iterate over the Imports
|
|
for (const reqType of requiredImports) {
|
|
if (mappingTypeToImport[reqType] && mappingTypeToImport[reqType].size === 1) {
|
|
// Extract the Path of the File
|
|
const pathToFile = Array.from(mappingTypeToImport[reqType].values())[0] + '.ts';
|
|
|
|
const declarations = getDeclarationByName(fileMapping, pathToFile, reqType).filter(item => !importedBaseTypes.has(item.baseType));
|
|
declarations.map(item => importedBaseTypes.add(item.baseType));
|
|
|
|
item.imports.content += declarations.map(item =>
|
|
item.originalCode
|
|
).reduce(
|
|
(prev, current, idx) => item.imports.content.length === 0 && idx === 0 ? current : prev + '\n\n' + current, ''
|
|
);
|
|
|
|
} else if (mappingTypeToImport[reqType].size > 1) {
|
|
// Multiple Items are using the Same Name.
|
|
}
|
|
}
|
|
|
|
ret.push(item);
|
|
}
|
|
|
|
|
|
// Return the Type.
|
|
return ret;
|
|
} |