nope/lib/helpers/analyzeTypescriptFiles.ts

663 lines
23 KiB
TypeScript

import { ClassDeclaration, Decorator, MethodDeclaration, Node, Project, PropertyDeclaration, SourceFile, Type, InterfaceDeclaration, FunctionDeclaration, ExportedDeclarations } 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;
} & 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.
*/
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 = 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"
}
// 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
}
// 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;
}
}
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,
}
// // 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
}
}
}
/**
* 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 => 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) {
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 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[], options: {
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 (
options.classInterface === '' ||
isClassImplementingInterface(cl, options.classInterface, importMapping.aliasToOriginal)
) && (
options.classDecorator === '' ||
getDecorators(cl, options.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, options.methodDecorator, importMapping.aliasToOriginal))
.filter(methodObject => methodObject.decorators.length > 0 && getModifiers(methodObject.declaration as MethodDeclaration).isPublic);
// Parsed Method
const parsedMethods = sharedMethodsOfClass.map(methodObject => getDescription(methodObject.declaration as MethodDeclaration) as MethodInformation);
// Get the Properties
const relevantProperties = getMatchingProperties(relevantClass, options.propertyType, false)
.filter(property => property.isPublic &&
getDecorators(
property.declaration,
options.propertyDecorator,
importMapping.aliasToOriginal,
false,
false
)
);
const item = {
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');
}