441 lines
14 KiB
TypeScript
441 lines
14 KiB
TypeScript
|
import { ClassDeclaration, Decorator, MethodDeclaration, Node, Project, PropertyDeclaration, SourceFile, Type } from "ts-morph";
|
||
|
|
||
|
/**
|
||
|
* 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.
|
||
|
*/
|
||
|
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.
|
||
|
*/
|
||
|
export function getType(node: Node, inputType: Type, text: string) {
|
||
|
|
||
|
// Define return Properties.
|
||
|
let baseType = '';
|
||
|
let simplifiedType = '';
|
||
|
let simplifiedSubType = '';
|
||
|
let typeImports: { path: string, type: string }[] = [];
|
||
|
|
||
|
// 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 = externalTypes.exec(text);
|
||
|
|
||
|
// 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) {
|
||
|
|
||
|
|
||
|
|
||
|
// Update the Imported Types.
|
||
|
typeImports = 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 type = text.split(').')[1];
|
||
|
|
||
|
return {
|
||
|
path,
|
||
|
type
|
||
|
};
|
||
|
});
|
||
|
|
||
|
} else if (Node.isFunctionDeclaration(node) || Node.isFunctionTypeNode(node) || Node.isFunctionLikeDeclaration(node)) {
|
||
|
baseType = "function"
|
||
|
}
|
||
|
|
||
|
// Define the a Simplified Subtype.
|
||
|
simplifiedSubType = simplifiedType.slice(baseType.length + '<'.length, simplifiedType.length - 1);
|
||
|
}
|
||
|
|
||
|
// Define a Partial element of the Return-Value.
|
||
|
const ret: {
|
||
|
isBaseType: boolean,
|
||
|
baseType: string,
|
||
|
simplifiedType?: string,
|
||
|
simplifiedSubType?: string,
|
||
|
typeImports?: { path: string, type: string }[],
|
||
|
originalCode: string,
|
||
|
} = {
|
||
|
isBaseType: !!(inputType.compilerType as any).intrinsicName,
|
||
|
baseType,
|
||
|
originalCode: inputType.getText()
|
||
|
}
|
||
|
|
||
|
// 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() === getPropertyDescription(prop).baseType.toLowerCase();
|
||
|
} else {
|
||
|
return reqType === getPropertyDescription(prop).baseType;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Function to generate a Property description.
|
||
|
* @param prop The Property Declaration.
|
||
|
*/
|
||
|
export function getPropertyDescription(prop: PropertyDeclaration) {
|
||
|
|
||
|
return Object.assign(
|
||
|
// Use the Modifiers
|
||
|
getModifiers(prop),
|
||
|
Object.assign(
|
||
|
// the Type Description
|
||
|
getType(
|
||
|
prop.getTypeNode(),
|
||
|
prop.getType(),
|
||
|
prop.getText()
|
||
|
),
|
||
|
{
|
||
|
// And use the Propertiy Name.
|
||
|
name: prop.getName(),
|
||
|
// Keep the declartion object as well
|
||
|
declaration: prop
|
||
|
}
|
||
|
)
|
||
|
);
|
||
|
}
|
||
|
|
||
|
export function getMatchingProperties(cl: ClassDeclaration, reqType: string, caseSensitive = true) {
|
||
|
return cl.getProperties()
|
||
|
// Firstly Filter the Properties, that they match the requested Type.
|
||
|
.filter(prop => isPropOfType(
|
||
|
prop,
|
||
|
reqType,
|
||
|
caseSensitive))
|
||
|
// Instead of returning the Property Declaration, return the
|
||
|
// Property Descriptor.
|
||
|
.map(
|
||
|
propertyDeclaration => getPropertyDescription(propertyDeclaration)
|
||
|
);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 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, decorator: string, aliasToOriginal: { [index: string]: string }, caseSensitive = true, extractArgs = true) {
|
||
|
|
||
|
if (!caseSensitive) {
|
||
|
decorator = decorator.toLowerCase();
|
||
|
}
|
||
|
|
||
|
let decoratorSettings: { [index: string]: any }[] = [];
|
||
|
|
||
|
let decorators: Decorator[] = declaration.getDecorators()
|
||
|
.filter(usedDecorator => {
|
||
|
let name = usedDecorator.getName();
|
||
|
if (!caseSensitive) {
|
||
|
name = name.toLowerCase();
|
||
|
}
|
||
|
|
||
|
if (extractArgs) {
|
||
|
// Try to extract the arguments of the Decorator (doest work on none static object / elements)
|
||
|
usedDecorator.getArguments().map(a => {
|
||
|
// Parse the Text
|
||
|
const text = a.getText();
|
||
|
|
||
|
// Bad Practice. Create the Code create Function that will create the Object.
|
||
|
try {
|
||
|
decoratorSettings.push(eval('() => { return ' + text + '}')());
|
||
|
} catch (e) {
|
||
|
// Failed to Parse
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
// If the Decorator uses an alias => return
|
||
|
// the test with the original name of the decorator.
|
||
|
if (typeof aliasToOriginal[name] === 'string') {
|
||
|
return decorator === aliasToOriginal[name]
|
||
|
}
|
||
|
|
||
|
return decorator === name;
|
||
|
});
|
||
|
|
||
|
return {
|
||
|
declaration,
|
||
|
decorators,
|
||
|
decoratorSettings
|
||
|
};
|
||
|
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 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 getMethodInfo(declaration: MethodDeclaration) {
|
||
|
|
||
|
const abstract = declaration.isAbstract();
|
||
|
const isAsync = declaration.isAsync();
|
||
|
const name = declaration.getName();
|
||
|
const isGenerator = declaration.isGenerator();
|
||
|
const isImplementation = declaration.isImplementation();
|
||
|
|
||
|
const retTypeObj = declaration.getReturnType();
|
||
|
|
||
|
const returnType = getType(
|
||
|
declaration.getReturnTypeNode(),
|
||
|
retTypeObj,
|
||
|
retTypeObj.getText()
|
||
|
)
|
||
|
// Extract the Parameters of the Function
|
||
|
const params = 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,
|
||
|
},
|
||
|
getType(
|
||
|
parameter.getTypeNode(),
|
||
|
parameter.getType(),
|
||
|
parameter.getText()
|
||
|
)
|
||
|
)
|
||
|
});
|
||
|
|
||
|
return {
|
||
|
declaration,
|
||
|
params,
|
||
|
abstract,
|
||
|
name,
|
||
|
isAsync,
|
||
|
isGenerator,
|
||
|
isImplementation,
|
||
|
returnType
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Function to extrac the Modifiers of a declaration.
|
||
|
* @param declaration
|
||
|
*/
|
||
|
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')
|
||
|
}
|
||
|
|
||
|
return {
|
||
|
declaration,
|
||
|
modifiers,
|
||
|
isPublic: modifiers.includes('public'),
|
||
|
isPrivate: modifiers.includes('private'),
|
||
|
isProtected: modifiers.includes('protected'),
|
||
|
isReadonly: modifiers.includes('readonly'),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
// Function to Determine new project files.
|
||
|
const project = new Project({
|
||
|
tsConfigFilePath: "./tsconfigBackend.json",
|
||
|
addFilesFromTsConfig: false,
|
||
|
});
|
||
|
project.addSourceFileAtPath("./dist/test.ts");
|
||
|
const testSourceFiles = project.getSourceFiles()
|
||
|
const file = testSourceFiles[0];
|
||
|
|
||
|
// Create a Mapping File.
|
||
|
const importMapping = getImportsOfFile(file);
|
||
|
const injectedClasses = file.getClasses().filter(cl => isClassImplementingInterface(cl, 'ISharedObservables', importMapping.aliasToOriginal));
|
||
|
|
||
|
// Extract the Methods.
|
||
|
const sharedMethods = injectedClasses[0].getMethods()
|
||
|
.map(method => getDecorators(method, 'online', importMapping.aliasToOriginal))
|
||
|
.filter(methodObject => methodObject.decorators.length > 0 && getModifiers(methodObject.declaration).isPublic);
|
||
|
|
||
|
const parsedMethods = sharedMethods.map(methodObject => getMethodInfo(methodObject.declaration as MethodDeclaration));
|
||
|
|
||
|
// Get the Properties
|
||
|
const sharedObservables = getMatchingProperties(injectedClasses[0], 'IObservable', false)
|
||
|
.filter(property =>
|
||
|
property.isPublic &&
|
||
|
getDecorators(property.declaration, 'inject', importMapping.aliasToOriginal, false, false)
|
||
|
);
|
||
|
|
||
|
console.log('Done');
|