import { ClassDeclaration, Decorator, MethodDeclaration, Node, Project, PropertyDeclaration, SourceFile, Type } from "ts-morph"; export type TypeInformation = { isBaseType: boolean; baseType: string; simplifiedType?: string; simplifiedSubType?: string; typeImports?: { path: string; type: 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; } & TypeInformation); export type MethodInformation = { declaration: MethodDeclaration; params: ParameterInformation[]; isAbstract: boolean; name: string; isAsync: boolean; isGenerator: boolean; isImplementation: boolean; returnType: TypeInformation; } 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. */ 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: TypeInformation = { 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): PropertyInformation { 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 } ) ); } /** * 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, reqType: string, caseSensitive = true): PropertyInformation[] { 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 | ClassDeclaration, 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; } /** * Helper Function to extract the Information of a Method. * @param declaration Declartion of the Method */ export function getMethodInfo(declaration: MethodDeclaration): MethodInformation { const isAbstract = 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: 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, }, getType( parameter.getTypeNode(), parameter.getType(), parameter.getText() ) ) }); return { declaration, params, isAbstract, name, isAsync, isGenerator, isImplementation, returnType } } /** * 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; } /** * 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[], classDecorator: string, classInterface: string, methodDecorator: string, propertyType: string, propertyDecorator: string,) { const ret: { className: string; methods: MethodInformation[]; properties: PropertyInformation[]; }[] = []; // 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 => { return ( classInterface === '' || isClassImplementingInterface(cl, classInterface, importMapping.aliasToOriginal) ) && ( classDecorator === '' || getDecorators(cl, classDecorator, importMapping.aliasToOriginal) ) }); // 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, methodDecorator, importMapping.aliasToOriginal)) .filter(methodObject => methodObject.decorators.length > 0 && getModifiers(methodObject.declaration as MethodDeclaration).isPublic); // Parsed Method const parsedMethods = sharedMethodsOfClass.map(methodObject => getMethodInfo(methodObject.declaration as MethodDeclaration)); // Get the Properties const relevantProperties = getMatchingProperties(relevantClass, propertyType, false) .filter(property => property.isPublic && getDecorators(property.declaration, propertyDecorator, importMapping.aliasToOriginal, false, false) ); const item = { className: relevantClass.getName(), methods: parsedMethods, properties: relevantProperties }; ret.push(item) } } return ret; }