diff --git a/lib/helpers/analyzeTypescriptFiles.ts b/lib/helpers/analyzeTypescriptFiles.ts index fb72eb0..89ac7c6 100644 --- a/lib/helpers/analyzeTypescriptFiles.ts +++ b/lib/helpers/analyzeTypescriptFiles.ts @@ -1,4 +1,4 @@ -import { ClassDeclaration, Decorator, MethodDeclaration, Node, Project, PropertyDeclaration, SourceFile, Type } from "ts-morph"; +import { ClassDeclaration, Decorator, MethodDeclaration, Node, Project, PropertyDeclaration, SourceFile, Type, InterfaceDeclaration, FunctionDeclaration, ExportedDeclarations } from "ts-morph"; export type TypeInformation = { isBaseType: boolean; @@ -7,7 +7,7 @@ export type TypeInformation = { simplifiedSubType?: string; typeImports?: { path: string; - type: string; + identifier: string; }[]; originalCode: string; } @@ -145,13 +145,14 @@ export function getImplementedInterface(cl: ClassDeclaration, aliasToOriginal: { * @param inputType The type * @param text The textual representation. */ -export function getType(node: Node, inputType: Type, text: string) { +function _getType(node: Node, inputType: Type, text: string) { // Define return Properties. let baseType = ''; let simplifiedType = ''; let simplifiedSubType = ''; - let typeImports: { path: string, type: string }[] = []; + let typeImports: { path: string, identifier: string }[] = []; + let originalCode = inputType.getText(); // Test if the Type is a Base-Type. if ((inputType.compilerType as any).intrinsicName) { @@ -165,16 +166,14 @@ export function getType(node: Node, inputType: Type, text: string) { // Regex to extrat the Imports with the corresponding type. const externalTypes = /import\(.+?\)\.\w+/g; - const result = externalTypes.exec(text); + 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) { - - + if (result.length > 0) { // Update the Imported Types. typeImports = result.map(item => { @@ -184,27 +183,30 @@ export function getType(node: Node, inputType: Type, text: string) { const regex = /(?<=")(.*)(?=")/g; const path = regex.exec(text)[0].toString(); // Get the corresponding Typ-Identifier - const type = text.split(').')[1]; + const identifier = text.split(').')[1]; return { path, - type + identifier }; }); } 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); + 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: inputType.getText() + originalCode } // Add the Additional elements if required. @@ -227,36 +229,130 @@ export function getType(node: Node, inputType: Type, text: string) { */ export function isPropOfType(prop: PropertyDeclaration, reqType: string, caseSensitive = true) { if (!caseSensitive) { - return reqType.toLowerCase() === getPropertyDescription(prop).baseType.toLowerCase(); + return reqType.toLowerCase() === (getDescription(prop) as PropertyInformation).baseType.toLowerCase(); } else { - return reqType === getPropertyDescription(prop).baseType; + return reqType === (getDescription(prop) as PropertyInformation).baseType; } } -/** - * Function to generate a Property description. - * @param prop The Property Declaration. - */ -export function getPropertyDescription(prop: PropertyDeclaration): PropertyInformation { +export function getDescription(declaration: PropertyDeclaration | InterfaceDeclaration | FunctionDeclaration | MethodDeclaration | ExportedDeclarations): PropertyInformation | MethodInformation | TypeInformation { - 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 + 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, + } + + // // Add the Additional elements if required. + // if (!ret.isBaseType) { + // ret.simplifiedType = simplifiedType; + // ret.simplifiedSubType = simplifiedSubType; + // ret.typeImports = typeImports; + // } + + // // Return the Type. + return ret; + + } else if (Node.isFunctionDeclaration(declaration)) { + + } else if (Node.isMethodDeclaration(declaration)) { + 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 + } + } } /** @@ -275,7 +371,7 @@ export function getMatchingProperties(cl: ClassDeclaration, reqType: string, cas // Instead of returning the Property Declaration, return the // Property Descriptor. .map( - propertyDeclaration => getPropertyDescription(propertyDeclaration) + propertyDeclaration => getDescription(propertyDeclaration) as PropertyInformation ); } @@ -351,57 +447,6 @@ export function isPropertyInjectedWith(prop: PropertyDeclaration, decorator: str 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 @@ -459,7 +504,13 @@ export function getModifiers(declaration: MethodDeclaration | PropertyDeclaratio * @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,) { +export function analyzeClasses(sources: SourceFile[], options: { + classDecorator: string, + classInterface: string, + methodDecorator: string, + propertyType: string, + propertyDecorator: string, +}) { const ret: { className: string; @@ -476,11 +527,11 @@ export function analyzeClasses(sources: SourceFile[], classDecorator: string, cl // 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) + options.classInterface === '' || + isClassImplementingInterface(cl, options.classInterface, importMapping.aliasToOriginal) ) && ( - classDecorator === '' || - getDecorators(cl, classDecorator, importMapping.aliasToOriginal) + options.classDecorator === '' || + getDecorators(cl, options.classDecorator, importMapping.aliasToOriginal) ) }); @@ -489,17 +540,22 @@ export function analyzeClasses(sources: SourceFile[], classDecorator: string, cl for (const relevantClass of relevantClasses) { // Extract the Methods. const sharedMethodsOfClass = relevantClass.getMethods() - .map(method => getDecorators(method, methodDecorator, importMapping.aliasToOriginal)) + .map(method => getDecorators(method, options.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)); + const parsedMethods = sharedMethodsOfClass.map(methodObject => getDescription(methodObject.declaration as MethodDeclaration) as MethodInformation); // Get the Properties - const relevantProperties = getMatchingProperties(relevantClass, propertyType, false) - .filter(property => - property.isPublic && - getDecorators(property.declaration, propertyDecorator, importMapping.aliasToOriginal, false, false) + const relevantProperties = getMatchingProperties(relevantClass, options.propertyType, false) + .filter(property => property.isPublic && + getDecorators( + property.declaration, + options.propertyDecorator, + importMapping.aliasToOriginal, + false, + false + ) ); const item = { @@ -513,4 +569,95 @@ export function analyzeClasses(sources: SourceFile[], classDecorator: string, cl } 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'); } \ No newline at end of file