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 FunctionInformation = { declaration: FunctionDeclaration; params: ParameterInformation[]; name: string; isAsync: boolean; isGenerator: boolean; isImplementation: boolean; returnType: TypeInformation; hasReturnType: boolean; head: string; authorDescription: string; } export type FunctionDeclaredInVariableInformation = ({ declarationCode: string, } & MethodInformation & TypeInformation) export type MethodInformation = { declaration: MethodDeclaration; params: ParameterInformation[]; isAbstract: boolean; name: string; isAsync: boolean; isGenerator: boolean; isImplementation: boolean; returnType: TypeInformation; hasReturnType: boolean; 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 IClassAnalyzeResult { // 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 interface IExportedFunctionResult extends FunctionDeclaredInVariableInformation { imports: { content: string, required: boolean, } } /** * Interface for the Analyzing Result */ export interface IAnalyzeResult { classes: IClassAnalyzeResult[], functions: (IExportedFunctionResult & { className: string })[] } export type PropertyInformation = DecoratorInformation & 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 as any).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 }; })); } // The Element could be a Function. 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 | FunctionInformation | FunctionDeclaredInVariableInformation { 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 as any).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() ); const hasReturnType = returnType.simplifiedSubType !== 'void'; // 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) => { // Based on the Fact, whether the Parameter is a Function or not // The inputs of the the _getType Function has to be adapted. const isFunc = Node.isMethodDeclaration(parameter) 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(), isFunc ? parameter.getText() : parameter.getType().getText() ) ) }); return { declaration, params, isAbstract, head, name, isAsync, isGenerator, isImplementation, returnType, hasReturnType, authorDescription } } else if (Node.isFunctionDeclaration(declaration)) { const name = declaration.getName(); const isGenerator = declaration.isGenerator(); const isImplementation = declaration.isImplementation(); const retTypeObj = declaration.getReturnType(); const returnType = _getType( declaration.getReturnTypeNode(), retTypeObj, retTypeObj.getText() ); const hasReturnType = returnType.simplifiedSubType !== 'void'; // 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) => { // Based on the Fact, whether the Parameter is a Function or not // The inputs of the the _getType Function has to be adapted. const isFunc = Node.isMethodDeclaration(parameter) 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(), isFunc ? parameter.getText() : parameter.getType().getText() ) ) }); return { declaration, params, head, name, isAsync, isGenerator, isImplementation, returnType, hasReturnType, authorDescription } } else if (Node.isVariableDeclaration(declaration)) { const name = declaration.getName(); // Use the Regex to remove the Imports from the Head const regex = /import\(.+?\)./g // Extract the Description of the Author. const authorDescription = declaration.getLeadingCommentRanges().map(comment => comment.getText()).join('\n'); const typeInformation = _getType( declaration.getTypeNode(), declaration.getType(), declaration.getType().getText() ); // Define the Method Head. const _head = declaration.getType().getText().replace(regex, ''); const head = _head.slice(1, _head.lastIndexOf('=>') - 2); let returnType = _head.slice(_head.lastIndexOf('=>') + 3); if (returnType.startsWith('Promise<')) { returnType = returnType.slice('Promise<'.length, returnType.length - 1) } return Object.assign( // the Type Description typeInformation, { // And use the Propertiy Name. name, // Keep the declartion object as well declaration, // Provide Author Information authorDescription, // Original Declaration code. declarationCode: declaration.getText(), // Provide the head head, // Provide the returning type returnType, } ) } } /** * 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 } } /** * Method to create a Default Filter for Methods. * @param options */ export function defaultFunctionFilter(options: { functionDecorator: string, }) { return (func: FunctionDeclaredInVariableInformation) => func.declarationCode.includes(options.functionDecorator + '('); } /** * 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, filterFunctions: (func: FunctionDeclaredInVariableInformation) => boolean, checkImport: (type: string) => boolean, classDecorator: string, methodDecorator: string, propertyDecorator: string, } = { // Define some Default settings classDecorator: 'exportsElementsToDispatcher', methodDecorator: 'exportMethodToDispatcher', propertyDecorator: 'exportPropertyToDispatcher', filterClasses: defaultClassFilter({ classDecorator: 'exportsElementsToDispatcher', classInterface: '' }), filterMethods: defaultMethodFilter(), filterFunctions: defaultFunctionFilter({ functionDecorator: 'exportFunctionToDispatcher' }), filterProperties: defaultPropFilter({ propertyDecorator: 'exportPropertyToDispatcher', propertyType: 'nopeObservable' }), checkImport: type => type !== 'nopeObservable', }) { const fileMapping = createFileMapping(sourceFiles); const classes = analyzeClasses(sourceFiles, options); const ret: IAnalyzeResult = { classes: [], functions: [] } /** * Iterate over the SourceFiles. * * For every source File, check if there exists a * method. (The methods are wrapped by a decorator * Function. ==> This results in detecting exported * Variables.) If so, filter the Methods and extract * the descriptor. */ for (const file of sourceFiles) { const functionsWithDecorators = Array.from(file.getExportedDeclarations().values()).map(value => { // Extract the Element const declaration = value[0]; if (declaration && Node.isVariableDeclaration(declaration)) { const name = declaration.getName(); // Use the Regex to remove the Imports from the Head const regex = /import\(.+?\)./g // Extract the Description of the Author. const authorDescription = declaration.getLeadingCommentRanges().map(comment => comment.getText()).join('\n'); const typeInformation = _getType( declaration.getTypeNode(), declaration.getType(), declaration.getType().getText() ); if (!typeInformation.isBaseType) { // Get the Defintion of the Function. const funcDefintion = file.getFunction(typeInformation.baseType); if (funcDefintion) { const ret = Object.assign( getDescription(funcDefintion) as MethodInformation, { // Provide Author Information authorDescription, // Original Declaration code. declarationCode: declaration.getText() }, _getType( funcDefintion, funcDefintion.getType(), funcDefintion.getText() ), { name } ) as FunctionDeclaredInVariableInformation; return ret; // return Object.assign( // getDescription(funcDefintion) as MethodInformation, // { // // Provide Author Information // authorDescription, // // Original Declaration code. // declarationCode: declaration.getText() // }, // typeInformation // ) as FunctionDeclaredInVariableInformation; } } } // Return if the Element is a Declaration or not. return false }).filter(value => { // Instead of returnin the declaration return its descriptor. return value; }).filter( item => // Additional Filter the Methods with custom filtering method. options.filterFunctions(item as FunctionDeclaredInVariableInformation) ) as FunctionDeclaredInVariableInformation[]; const requiredImports = new Set(); const mappingTypeToImport: { [index: string]: Set } = {} for (const func of functionsWithDecorators) { for (const { identifier, path } of (func.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(); } mappingTypeToImport[identifier].add(path); requiredImports.add(identifier); } } } for (const func of functionsWithDecorators) { const item: (IExportedFunctionResult & { className: string }) = Object.assign({ imports: { content: '', required: false, }, className: func.name }, func); // List containing the already used Types const importedBaseTypes = new Set(); // 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. } } // Determine Imports item.imports.required = requiredImports.size > 0; ret.functions.push(item); } } // Iterate over the Classes for (const relevantClass of classes) { // Reurn Result of a class. const item: IClassAnalyzeResult = { className: relevantClass.className, classDecorator: relevantClass.decorator, methods: [], properties: [], imports: { content: '', required: false } } const requiredImports = new Set(); const mappingTypeToImport: { [index: string]: Set } = {} // 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(); } 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(); } 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(); } 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(); // 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.classes.push(item); } // Return the Type. return ret; }