Build code introspection service (#7760)

Starting to use ts-morph to retrieve function parameters
This commit is contained in:
Thomas Trompette
2024-10-17 15:08:42 +02:00
committed by GitHub
parent ddbfabfc99
commit f338d01b4f
7 changed files with 278 additions and 7 deletions

View File

@ -44,6 +44,7 @@
"monaco-editor-auto-typings": "^0.4.5",
"passport": "^0.7.0",
"psl": "^1.9.0",
"ts-morph": "^24.0.0",
"tsconfig-paths": "^4.2.0",
"typeorm": "patch:typeorm@0.3.20#./patches/typeorm+0.3.20.patch",
"unzipper": "^0.12.3",

View File

@ -4,19 +4,24 @@ import { InjectRepository } from '@nestjs/typeorm';
import { basename, dirname, join } from 'path';
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
import { Repository } from 'typeorm';
import deepEqual from 'deep-equal';
import { Repository } from 'typeorm';
import { FileStorageExceptionCode } from 'src/engine/core-modules/file-storage/interfaces/file-storage-exception';
import { ServerlessExecuteResult } from 'src/engine/core-modules/serverless/drivers/interfaces/serverless-driver.interface';
import { ThrottlerService } from 'src/engine/core-modules/throttler/throttler.service';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { readFileContent } from 'src/engine/core-modules/file-storage/utils/read-file-content';
import { ENV_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/constants/env-file-name';
import { INDEX_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/constants/index-file-name';
import { LAST_LAYER_VERSION } from 'src/engine/core-modules/serverless/drivers/layers/last-layer-version';
import { getBaseTypescriptProjectFiles } from 'src/engine/core-modules/serverless/drivers/utils/get-base-typescript-project-files';
import { getLastLayerDependencies } from 'src/engine/core-modules/serverless/drivers/utils/get-last-layer-dependencies';
import { ServerlessService } from 'src/engine/core-modules/serverless/serverless.service';
import { getServerlessFolder } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils';
import { ThrottlerService } from 'src/engine/core-modules/throttler/throttler.service';
import { CreateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/create-serverless-function.input';
import { UpdateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/update-serverless-function.input';
import {
ServerlessFunctionEntity,
@ -27,11 +32,6 @@ import {
ServerlessFunctionExceptionCode,
} from 'src/engine/metadata-modules/serverless-function/serverless-function.exception';
import { isDefined } from 'src/utils/is-defined';
import { getLastLayerDependencies } from 'src/engine/core-modules/serverless/drivers/utils/get-last-layer-dependencies';
import { LAST_LAYER_VERSION } from 'src/engine/core-modules/serverless/drivers/layers/last-layer-version';
import { CreateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/create-serverless-function.input';
import { getBaseTypescriptProjectFiles } from 'src/engine/core-modules/serverless/drivers/utils/get-base-typescript-project-files';
import { ENV_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/constants/env-file-name';
@Injectable()
export class ServerlessFunctionService extends TypeOrmQueryService<ServerlessFunctionEntity> {

View File

@ -0,0 +1,106 @@
import { Test, TestingModule } from '@nestjs/testing';
import { CodeIntrospectionException } from 'src/modules/code-introspection/code-introspection.exception';
import { CodeIntrospectionService } from 'src/modules/code-introspection/code-introspection.service';
describe('CodeIntrospectionService', () => {
let service: CodeIntrospectionService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [CodeIntrospectionService],
}).compile();
service = module.get<CodeIntrospectionService>(CodeIntrospectionService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('analyze', () => {
it('should analyze a function declaration correctly', () => {
const fileContent = `
function testFunction(param1: string, param2: number): void {
console.log(param1, param2);
}
`;
const result = service.analyze(fileContent);
expect(result).toEqual([
{ name: 'param1', type: 'string' },
{ name: 'param2', type: 'number' },
]);
});
it('should analyze an arrow function correctly', () => {
const fileContent = `
const testArrowFunction = (param1: string, param2: number): void => {
console.log(param1, param2);
};
`;
const result = service.analyze(fileContent);
expect(result).toEqual([
{ name: 'param1', type: 'string' },
{ name: 'param2', type: 'number' },
]);
});
it('should return an empty array for files without functions', () => {
const fileContent = `
const x = 5;
console.log(x);
`;
const result = service.analyze(fileContent);
expect(result).toEqual([]);
});
it('should throw an exception for multiple function declarations', () => {
const fileContent = `
function func1(param1: string) {}
function func2(param2: number) {}
`;
expect(() => service.analyze(fileContent)).toThrow(
CodeIntrospectionException,
);
expect(() => service.analyze(fileContent)).toThrow(
'Only one function is allowed',
);
});
it('should throw an exception for multiple arrow functions', () => {
const fileContent = `
const func1 = (param1: string) => {};
const func2 = (param2: number) => {};
`;
expect(() => service.analyze(fileContent)).toThrow(
CodeIntrospectionException,
);
expect(() => service.analyze(fileContent)).toThrow(
'Only one arrow function is allowed',
);
});
it('should correctly analyze complex types', () => {
const fileContent = `
function complexFunction(param1: string[], param2: { key: number }): Promise<boolean> {
return Promise.resolve(true);
}
`;
const result = service.analyze(fileContent);
expect(result).toEqual([
{ name: 'param1', type: 'string[]' },
{ name: 'param2', type: '{ key: number; }' },
]);
});
});
});

View File

@ -0,0 +1,12 @@
import { CustomException } from 'src/utils/custom-exception';
export class CodeIntrospectionException extends CustomException {
code: CodeIntrospectionExceptionCode;
constructor(message: string, code: CodeIntrospectionExceptionCode) {
super(message, code);
}
}
export enum CodeIntrospectionExceptionCode {
ONLY_ONE_FUNCTION_ALLOWED = 'ONLY_ONE_FUNCTION_ALLOWED',
}

View File

@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { CodeIntrospectionService } from 'src/modules/code-introspection/code-introspection.service';
@Module({
providers: [CodeIntrospectionService],
exports: [CodeIntrospectionService],
})
export class CodeIntrospectionModule {}

View File

@ -0,0 +1,92 @@
import { Injectable } from '@nestjs/common';
import {
ArrowFunction,
FunctionDeclaration,
ParameterDeclaration,
Project,
SyntaxKind,
} from 'ts-morph';
import {
CodeIntrospectionException,
CodeIntrospectionExceptionCode,
} from 'src/modules/code-introspection/code-introspection.exception';
type FunctionParameter = {
name: string;
type: string;
};
@Injectable()
export class CodeIntrospectionService {
private project: Project;
constructor() {
this.project = new Project();
}
public analyze(
fileContent: string,
fileName = 'temp.ts',
): FunctionParameter[] {
const sourceFile = this.project.createSourceFile(fileName, fileContent, {
overwrite: true,
});
const functionDeclarations = sourceFile.getFunctions();
if (functionDeclarations.length > 0) {
return this.analyzeFunctions(functionDeclarations);
}
const arrowFunctions = sourceFile.getDescendantsOfKind(
SyntaxKind.ArrowFunction,
);
if (arrowFunctions.length > 0) {
return this.analyzeArrowFunctions(arrowFunctions);
}
return [];
}
private analyzeFunctions(
functionDeclarations: FunctionDeclaration[],
): FunctionParameter[] {
if (functionDeclarations.length > 1) {
throw new CodeIntrospectionException(
'Only one function is allowed',
CodeIntrospectionExceptionCode.ONLY_ONE_FUNCTION_ALLOWED,
);
}
const functionDeclaration = functionDeclarations[0];
return functionDeclaration.getParameters().map(this.buildFunctionParameter);
}
private analyzeArrowFunctions(
arrowFunctions: ArrowFunction[],
): FunctionParameter[] {
if (arrowFunctions.length > 1) {
throw new CodeIntrospectionException(
'Only one arrow function is allowed',
CodeIntrospectionExceptionCode.ONLY_ONE_FUNCTION_ALLOWED,
);
}
const arrowFunction = arrowFunctions[0];
return arrowFunction.getParameters().map(this.buildFunctionParameter);
}
private buildFunctionParameter(
parameter: ParameterDeclaration,
): FunctionParameter {
return {
name: parameter.getName(),
type: parameter.getType().getText(),
};
}
}