diff --git a/.devcontainer/devcontainer.json b/.devcontainer.json similarity index 59% rename from .devcontainer/devcontainer.json rename to .devcontainer.json index 2be28e9..5675e99 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer.json @@ -1,14 +1,12 @@ { - "name": "CLDK TypeScript SDK (Bun)", - "image": "oven/bun:latest", + "name": "CLDK TypeScript SDK (Node+Bun+Java)", + "image": "mcr.microsoft.com/devcontainers/javascript-node:20-bookworm", "features": { - "ghcr.io/devcontainers/features/node:1": { - "version": "20" - }, "ghcr.io/devcontainers/features/java:1": { "version": "11" } }, + "postCreateCommand": "curl -fsSL https://bun.sh/install | bash && export BUN_INSTALL=$HOME/.bun && export PATH=$BUN_INSTALL/bin:$PATH && bun install", "customizations": { "vscode": { "extensions": [ @@ -23,9 +21,5 @@ } } }, - "postCreateCommand": "bun install", - "workspaceFolder": "/workspace", - "mounts": [ - "source=${localWorkspaceFolder},target=/workspace,type=bind" - ] + "workspaceFolder": "/workspaces/typescript-sdk" } \ No newline at end of file diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 0000000..42a77bd --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,26 @@ +name: Test and Coverage + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Bun + uses: oven-sh/setup-bun@v1 + + - name: Install dependencies + run: bun install + + - name: Run tests with coverage + run: bun run test:withCoverage + + - name: Upload coverage to Coveralls + uses: coverallsapp/github-action@v2 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 254fb59..ecb25dd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -43,6 +43,9 @@ jobs: continue-on-error: false run: bun run test + - name: Upload coverage to Coveralls + uses: coverallsapp/github-action@v2 + - name: Generate Changelog id: gen_changelog uses: mikepenz/release-changelog-builder-action@v5 diff --git a/.prettierrc b/.prettierrc index e69de29..be3ac71 100644 --- a/.prettierrc +++ b/.prettierrc @@ -0,0 +1,3 @@ +{ + "printWidth": 120 +} \ No newline at end of file diff --git a/README.md b/README.md index ec189c4..6c43646 100644 --- a/README.md +++ b/README.md @@ -5,44 +5,43 @@

- - - - - - - - - - - - - - - + + + + + + + + + + + +

- **A framework that bridges the gap between traditional program analysis tools and Large Language Models (LLMs) specialized for code (CodeLLMs).** ### Overview + This is the TypeScript SDK for the Codellm-Devkit (CLDK). The SDK provides a unified interface for integrating outputs from various analysis tools and preparing them for effective use by CodeLLMs. It allows developers to streamline the process of transforming raw code into actionable insights. ### 📦 Installation - To install the SDK, you can use bun, npm, or yarn. Run the following command in your terminal: - +To install the SDK, you can use bun, npm, or yarn. Run the following command in your terminal: + 1. Using npm + ```bash npm i @codellm-devkit/cldk ``` 2. Using yarn + ```bash yarn add @codellm-devkit/cldk ``` -3. Using bun +3. Using bun ```bash bun add @codellm-devkit/cldk ``` @@ -65,7 +64,7 @@ This is the TypeScript SDK for the Codellm-Devkit (CLDK). The SDK provides a uni This creates a minimal `package.json` instantly. 3. Install `@codellm-devkit/cldk` - + ```bash bun add @codellm-devkit/cldk ``` @@ -74,17 +73,17 @@ This is the TypeScript SDK for the Codellm-Devkit (CLDK). The SDK provides a uni ```typescript import { CLDK } from "cldk"; - + // Initialize Java analysis const analysis = CLDK.for("java").analysis({ - projectPath: "/path/to/your/java/project", - analysisLevel: "Symbol Table", + projectPath: "/path/to/your/java/project", + analysisLevel: "Symbol Table", }); - + // Retrieve structured application model const jApplication = await analysis.getApplication(); console.log("Parsed JApplication:", jApplication); - + // Retrieve the symbol table const symbolTable = await analysis.getSymbolTable(); console.log("Symbol Table:", symbolTable); @@ -101,6 +100,7 @@ This is the TypeScript SDK for the Codellm-Devkit (CLDK). The SDK provides a uni #### Developing Locally (with Bun) 1. Clone the repository: + ```bash git clone https://github.com/codellm-devkit/typescript-sdk.git cd typescript-sdk @@ -112,6 +112,7 @@ This is the TypeScript SDK for the Codellm-Devkit (CLDK). The SDK provides a uni ``` _Note: follow any post-installation instructions to complete the installation_ 3. Install the dependencies + ```bash bun install ``` @@ -126,8 +127,8 @@ This is the TypeScript SDK for the Codellm-Devkit (CLDK). The SDK provides a uni 1. If you don't, ensure you have Docker/Podman and a compatible editor (e.g., VS Code) with the Dev Containers extension installed. 2. Open the repository in your editor. When prompted, reopen the project in the dev container. The devcontainer is configured to come pre-installed with bun and all the necessary dependencies. - + 3. You can start by run tests: ```bash bun run test - ``` \ No newline at end of file + ``` diff --git a/bunfig.toml b/bunfig.toml deleted file mode 100644 index 723324d..0000000 --- a/bunfig.toml +++ /dev/null @@ -1,5 +0,0 @@ -[test] -coverage = true -exclude = ["**/node_modules/**"] -patterns = ["**/*Test*.ts", "**/*test*.ts"] -timeout = 600000 # Sets timeout to 10 minutes \ No newline at end of file diff --git a/package.json b/package.json index bfb60aa..cbfb87c 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ "homepage": "https://github.com/codellm-devkit/typescript-sdk#readme", "scripts": { "build": "bun build ./src/index.ts --outdir ./dist", - "test": "bun test --preload ./test/conftest.ts --timeout=600000 --verbose", + "test": "bun test --verbose --coverage --preload ./test/conftest.ts --timeout=600000", + "test:withCoverage": "bun test --verbose --coverage --coverage-reporter=lcov --preload ./test/conftest.ts --timeout=600000", "clean": "rm -rf dist coverage *.lock" }, "files": [ @@ -36,10 +37,13 @@ "@types/jsonstream": "^0.8.33", "JSONStream": "^1.3.5", "bun": "^1.2.10", + "c8": "^10.1.3", + "chalk": "^5.4.1", "extract-zip": "^2.0.1", "fast-glob": "^3.3.3", "graphology": "^0.26.0", "loglevel": "^1.9.2", + "signale": "^1.4.0", "zod": "^3.24.3" }, "testing": { @@ -47,4 +51,4 @@ "c-test-applications-path": "./test-applications/c", "python-test-applications-path": "./test-applications/python" } -} +} \ No newline at end of file diff --git a/src/CLDK.ts b/src/CLDK.ts index 5e6c39b..8f1134f 100644 --- a/src/CLDK.ts +++ b/src/CLDK.ts @@ -1,57 +1,71 @@ -import {JavaAnalysis} from "./analysis/java"; -import {spawnSync} from "node:child_process"; +import { JavaAnalysis } from "./analysis/java"; +import { spawnSync } from "node:child_process"; export class CLDK { - /** - * The programming language of choice - */ - private language: string; + /** + * The programming language of choice + */ + private language: string; - constructor(language: string) { - this.language = language; - } + constructor(language: string) { + this.language = language; + } - /** - * A static for method to create a new instance of the CLDK class - */ - public static for(language: string): CLDK { - return new CLDK(language); - } + /** + * A static for method to create a new instance of the CLDK class + */ + public static for(language: string): CLDK { + return new CLDK(language); + } - /** - * Get the programming language of the CLDK instance - */ - public getLanguage(): string { - return this.language; - } + /** + * Get the programming language of the CLDK instance + */ + public getLanguage(): string { + return this.language; + } - /** - * Implementation of the analysis method - */ - public analysis({ projectPath, analysisLevel }: { projectPath: string, analysisLevel: string }): JavaAnalysis { - if (this.language === "java") { - this.makeSureJavaIsInstalled(); - return new JavaAnalysis({ - projectDir: projectPath, - analysisLevel: analysisLevel, - }); - } else { - throw new Error(`Analysis support for ${this.language} is not implemented yet.`); - } + /** + * Implementation of the analysis method + */ + public analysis({ + projectPath, + analysisLevel, + }: { + projectPath: string; + analysisLevel: string; + }): JavaAnalysis { + if (this.language === "java") { + this.makeSureJavaIsInstalled(); + return new JavaAnalysis({ + projectDir: projectPath, + analysisLevel: analysisLevel, + }); + } else { + throw new Error( + `Analysis support for ${this.language} is not implemented yet.` + ); } + } - private makeSureJavaIsInstalled(): Promise { - try { - const result = spawnSync("java", ["-version"], {encoding: "utf-8", stdio: "pipe"}); - if (result.error) { - throw result.error; - } - if (result.status !== 0) { - throw new Error(result.stderr || "Java is not installed. Please install Java 11+ to be able to analyze java projects."); - } - } catch (e: any) { - throw new Error(e.message || String(e)); - } - return Promise.resolve(); + private makeSureJavaIsInstalled(): Promise { + try { + const result = spawnSync("java", ["-version"], { + encoding: "utf-8", + stdio: "pipe", + }); + if (result.error) { + throw result.error; + } + if (result.status !== 0) { + throw new Error( + result.stderr || + "Java is not installed. Please install Java 11+ to be able to analyze java projects." + ); + } + } catch (e: any) { + throw new Error(e.message || String(e)); } + return Promise.resolve(); + } } diff --git a/src/analysis/java/JavaAnalysis.ts b/src/analysis/java/JavaAnalysis.ts index 3806fb2..9fc8c1a 100644 --- a/src/analysis/java/JavaAnalysis.ts +++ b/src/analysis/java/JavaAnalysis.ts @@ -17,145 +17,285 @@ import path from "path"; import fg from "fast-glob"; import fs from "fs"; -import log from "loglevel"; import { spawnSync } from "node:child_process"; -import { JApplication } from "../../models/java"; +import { JApplication, JCompilationUnit } from "../../models/java"; import * as types from "../../models/java/types"; +import { JType } from "../../models/java"; import os from "os"; -import JSONStream from "JSONStream"; import crypto from "crypto"; +import { createLogger } from "src/utils"; + +const logger = createLogger("JavaAnalysis"); enum AnalysisLevel { - SYMBOL_TABLE = "1", - CALL_GRAPH = "2", - SYSTEM_DEPENDENCY_GRAPH = "3", + SYMBOL_TABLE = "1", + CALL_GRAPH = "2", + SYSTEM_DEPENDENCY_GRAPH = "3", } const analysisLevelMap: Record = { - "symbol table": AnalysisLevel.SYMBOL_TABLE, - "call graph": AnalysisLevel.CALL_GRAPH, - "system dependency graph": AnalysisLevel.SYSTEM_DEPENDENCY_GRAPH + "symbol table": AnalysisLevel.SYMBOL_TABLE, + "call graph": AnalysisLevel.CALL_GRAPH, + "system dependency graph": AnalysisLevel.SYSTEM_DEPENDENCY_GRAPH, }; export class JavaAnalysis { - private readonly projectDir: string | null; - private analysisLevel: AnalysisLevel; - application?: types.JApplicationType; + private readonly projectDir: string | null; + private analysisLevel: AnalysisLevel; + application?: types.JApplicationType; - constructor(options: { projectDir: string | null; analysisLevel: string }) { - this.projectDir = options.projectDir; - this.analysisLevel = analysisLevelMap[options.analysisLevel.toLowerCase()] ?? AnalysisLevel.SYMBOL_TABLE; + constructor(options: { projectDir: string | null; analysisLevel: string }) { + this.projectDir = options.projectDir; + this.analysisLevel = analysisLevelMap[options.analysisLevel.toLowerCase()] ?? AnalysisLevel.SYMBOL_TABLE; + } - } + private getCodeAnalyzerExec(): string[] { + const codeanalyzerJarPath = path.resolve(__dirname, "jars"); + const pattern = path.join(codeanalyzerJarPath, "**/codeanalyzer-*.jar").replace(/\\/g, "/"); + const matches = fg.sync(pattern); + const jarPath = matches[0]; - private getCodeAnalyzerExec(): string[] { - const codeanalyzerJarPath = path.resolve(__dirname, "jars"); - const pattern = path.join(codeanalyzerJarPath, "**/codeanalyzer-*.jar").replace(/\\/g, "/"); - const matches = fg.sync(pattern); - const jarPath = matches[0]; - - if (!jarPath) { - console.log("Default codeanalyzer jar not found."); - throw new Error("Default codeanalyzer jar not found."); - } - log.info("Codeanalyzer jar found at:", jarPath); - return ["java", "-jar", jarPath]; + if (!jarPath) { + logger.error("Default codeanalyzer jar not found."); + throw new Error("Default codeanalyzer jar not found."); } + logger.info("Codeanalyzer jar found at:", jarPath); + return ["java", "-jar", jarPath]; + } + + /** + * Initialize the application by running the codeanalyzer and parsing the output. + * @private + * @returns {Promise} A promise that resolves to the parsed application data + * @throws {Error} If the project directory is not specified or if codeanalyzer fails + */ + private async _initialize_application(): Promise { + return new Promise((resolve, reject) => { + if (!this.projectDir) { + return reject(new Error("Project directory not specified")); + } + + const projectPath = path.resolve(this.projectDir); + // Create a temporary file to store the codeanalyzer output + const tmpFilePath = path.join(os.tmpdir(), `${Date.now()}-${crypto.randomUUID()}`); + const command = [ + ...this.getCodeAnalyzerExec(), + "--input", + projectPath, + "--output", + tmpFilePath, + `--analysis-level=${this.analysisLevel}`, + "--verbose", + ]; + // Check if command is valid + if (!command[0]) { + return reject(new Error("Codeanalyzer command not found")); + } + logger.debug(command.join(" ")); + const result = spawnSync(command[0], command.slice(1), { + stdio: ["ignore", "pipe", "inherit"], + }); + + if (result.error) { + return reject(result.error); + } - /** - * Initialize the application by running the codeanalyzer and parsing the output. - * @private - * @returns {Promise} A promise that resolves to the parsed application data - * @throws {Error} If the project directory is not specified or if codeanalyzer fails - */ - private async _initialize_application(): Promise { - return new Promise((resolve, reject) => { - if (!this.projectDir) { - return reject(new Error("Project directory not specified")); - } - - const projectPath = path.resolve(this.projectDir); - // Create a temporary file to store the codeanalyzer output - const tmpFilePath = path.join(os.tmpdir(), `${Date.now()}-${crypto.randomUUID()}`); - const command = [...this.getCodeAnalyzerExec(), "-i", projectPath, '-o', tmpFilePath, `--analysis-level=${this.analysisLevel}`, '--verbose']; - // Check if command is valid - if (!command[0]) { - return reject(new Error("Codeanalyzer command not found")); - } - log.debug(command.join(" ")); - const result = spawnSync(command[0], command.slice(1), { - stdio: ["ignore", "pipe", "inherit"], - }); - - if (result.error) { - return reject(result.error); - } - - if (result.status !== 0) { - return reject(new Error("Codeanalyzer failed to run.")); - } - - // Read the analysis result from the temporary file - try { - const stream = fs.createReadStream(path.join(tmpFilePath, 'analysis.json')).pipe(JSONStream.parse()); - const result = {} as types.JApplicationType; - - stream.on('data', (data: unknown) => { - Object.assign(result, JApplication.parse(data)); - }); - - stream.on('end', () => { - // Clean up the temporary file - fs.rm(tmpFilePath, {recursive: true, force: true}, (err) => { - if (err) log.warn(`Failed to delete temporary file: ${tmpFilePath}`, err); - }); - resolve(result as types.JApplicationType); - }); - - stream.on('error', (err: any) => { - reject(err); - }); - } catch (error) { - reject(error); - } + if (result.status !== 0) { + return reject(new Error("Codeanalyzer failed to run.")); + } + + // Read the analysis result from the temporary file + try { + const JSONStream = require("JSONStream"); + const stream = fs.createReadStream(path.join(tmpFilePath, "analysis.json")).pipe(JSONStream.parse()); + const result = {} as types.JApplicationType; + + stream.on("data", (data: unknown) => { + Object.assign(result, JApplication.parse(data)); + }); + + stream.on("end", () => { + // Clean up the temporary file + logger.debug(`Deleting temporary file: ${tmpFilePath}`); + fs.rm(tmpFilePath, { recursive: true, force: true }, (err) => { + if (err) logger.warn(`Failed to delete temporary file: ${tmpFilePath}`, err); + }); + resolve(result as types.JApplicationType); }); - } - /** - * Get the application data. This method returns the parsed Java application as a JSON structure containing the - * following information: - * |_ symbol_table: A record of file paths to compilation units. Each compilation unit further contains: - * |_ comments: Top-level file comments - * |_ imports: All import statements - * |_ type_declarations: All class/interface/enum/record declarations with their: - * |_ fields, methods, constructors, initialization blocks, etc. - * |_ call_graph: Method-to-method call relationships (if analysis level ≥ 2) - * |_ system_dependency_graph: System component dependencies (if analysis level = 3) - * - * The application view denoted by this application structure is crucial for further fine-grained analysis APIs. - * If the application is not already initialized, it will be initialized first. - * @returns {Promise} A promise that resolves to the application data - */ - public async getApplication(): Promise { - if (!this.application) { - this.application = await this._initialize_application(); - } - return this.application; + stream.on("error", (err: any) => { + reject(err); + }); + } catch (error) { + reject(error); + } + }); + } + + /** + * Get the application data. This method returns the parsed Java application as a JSON structure containing the + * following information: + * |_ symbol_table: A record of file paths to compilation units. Each compilation unit further contains: + * |_ comments: Top-level file comments + * |_ imports: All import statements + * |_ type_declarations: All class/interface/enum/record declarations with their: + * |_ fields, methods, constructors, initialization blocks, etc. + * |_ call_graph: Method-to-method call relationships (if analysis level ≥ 2) + * |_ system_dependency_graph: System component dependencies (if analysis level = 3) + * + * The application view denoted by this application structure is crucial for further fine-grained analysis APIs. + * If the application is not already initialized, it will be initialized first. + * @returns {Promise} A promise that resolves to the application data + */ + public async getApplication(): Promise { + if (!this.application) { + this.application = await this._initialize_application(); } + return this.application; + } + + /** + * Get the symbol table from the application. + * @returns {Promise>} A promise that resolves to a record of file paths and their + * corresponding {@link JCompilationUnitType} objects + * + * @notes This method retrieves the symbol table from the application, which contains information about all + * compilation units in the Java application. The returned record contains file paths as keys and their + * corresponding {@link JCompilationUnit} objects as values. + */ + public async getSymbolTable(): Promise> { + return (await this.getApplication()).symbol_table; + } - public async getSymbolTable(): Promise> { - return (await this.getApplication()).symbol_table; + /** + * Get all classes in the application. + * @returns {Promise>} A promise that resolves to a record of class names and their + * corresponding {@link JTypeType} objects + * + * @notes This method retrieves all classes from the symbol table and returns them as a record. The returned record + * contains class names as keys and their corresponding {@link JType} objects as values. + */ + public async getAllClasses(): Promise> { + return Object.values(await this.getSymbolTable()).reduce((classAccumulator, symbol) => { + Object.entries(symbol.type_declarations).forEach(([key, value]) => { + classAccumulator[key] = value; + }); + return classAccumulator; + }, {} as Record); + } + + /** + * Get a specific class by its qualified name. + * @param {string} qualifiedName - The qualified name of the class to retrieve + * @returns {Promise} A promise that resolves to the {@link JTypeType} object representing the class + * @throws {Error} If the class is not found in the application + * + * @notes This method retrieves a specific class from the application by its qualified name. If the class is found, + * it returns the corresponding {@link JType} object. If the class is not found, it throws an error. + */ + public async getClassByQualifiedName(qualifiedName: string): Promise { + const allClasses = await this.getAllClasses(); + if (allClasses[qualifiedName]) { + return allClasses[qualifiedName]; } + else + throw new Error(`Class ${qualifiedName} not found in the application.`); + } + + /** + * Get all methods in the application. + * @returns {Promise>>} A promise that resolves to a record of + * method names and their corresponding {@link JCallableType} objects + * + * @notes This method retrieves all methods from the symbol table and returns them as a record. The returned + * record contains class names as keys and their corresponding {@link JCallableType} objects as values. + * Each {@link JCallableType} object contains information about the method's parameters, return type, and + * other relevant details. + */ + public async getAllMethods(): Promise>> { + return Object.entries(await this.getAllClasses()).reduce((allMethods, [key, value]) => { + allMethods[key] = value.callable_declarations; + return allMethods; + }, {} as Record>); + } - public async getCallGraph(): Promise { - const application = await this.getApplication(); - if (application.call_graph === undefined || application.call_graph === null) { - log.debug("Re-initializing application with call graph"); - this.analysisLevel = AnalysisLevel.CALL_GRAPH; - this.application = await this._initialize_application(); - } + /** + * Get all methods in a specific class in the application. + * @returns {Promise>>} A promise that resolves to a record of + * method names and their corresponding {@link JCallableType} objects + * + * @notes This method retrieves all methods from the symbol table and returns them as a record. The returned + * record contains class names as keys and their corresponding {@link JCallableType} objects as values. + * Each {@link JCallableType} object contains information about the method's parameters, return type, and + * other relevant details. + */ + public async getAllMethodsByClass(qualifiedName: string): Promise> { + const classForWhichMethodsAreRequested = await this.getClassByQualifiedName(qualifiedName); + return classForWhichMethodsAreRequested ? Object.values(classForWhichMethodsAreRequested.callable_declarations ?? {}) : []; + } + /** + * Get a specific methods within a specific class by its qualified name. + * @param {string} qualifiedName - The qualified name of the class to retrieve + * @param {string} methodName - The name of the method to retrieve + * @returns {Promise} A promise that resolves to the {@link JCallable} object representing the method. + * @throws {Error} If the class or method is not found in the application. + * + * @notes This method retrieves a specific method from the application by its qualified name and method name. + * If the method is found, it returns the corresponding {@link JCallableType} object. If the method is not found, + * it throws an error. + */ + public async getMethodByQualifiedName(qualifiedName: string, methodName: string): Promise { + return (await this.getAllMethodsByClass(qualifiedName)).find( + (method) => method.signature === methodName + ) ?? (() => { throw new Error(`Method ${methodName} not found in class ${qualifiedName}.`); })(); + } + + /** + * Get all the method parameters in a specific method within a specific class by its qualified name. + * @param {string} qualifiedName - The qualified name of the class to retrieve + * @param {string} methodName - The name of the method to retrieve + * @returns {Promise>} A promise that resolves to an array of {@link JCallableParameterType} objects + * @throws {Error} If the class or method is not found in the application. + * + * @notes This method retrieves all the parameters of a specific method from the application by its qualified name + * and method name. If the method is found, it returns an array of {@link JCallableParameter} objects representing + */ + public async getMethodParameters(qualifiedName: string, methodName: string): Promise> { + return (await this.getMethodByQualifiedName(qualifiedName, methodName)).parameters ?? []; + } + + /** + * Get all the method parameters in a specific method within a specific class by its callable object. + * @param {types.JCallableType} callable - The callable object representing the method to retrieve + * @returns {Promise>} A promise that resolves to an array of {@link JCallableParameterType} objects + * + * @notes This method retrieves all the parameters of a specific method from the application by its callable object. + * If the method is found, it returns an array of {@link JCallableParameter} objects representing + * the parameters of the method. Otherwise, it returns an empty array. + */ + public async getMethodParametersFromCallable(callable: types.JCallableType): Promise> { + return callable.parameters ?? []; + } + + /** + * Get the java file path given the qualified name of the class. + * @param {string} qualifiedName - The qualified name of the class to retrieve + * @returns {Promise} A promise that resolves to the file path of the Java file containing the class + * @throws {Error} If the class is not found in the application. + * + * @notes This method retrieves the file path of the Java file containing the class with the specified qualified name. + * If the class is found, it returns the file path as a string. If the class is not found, it throws an error. + */ + public async getJavaFilePathByQualifiedName(qualifiedName: string): Promise { + const symbolTable = await this.getSymbolTable(); + for (const [filePath, compilationUnit] of Object.entries(symbolTable)) { + if (Object.keys(compilationUnit.type_declarations).includes(qualifiedName)) { + return filePath; + } } + throw new Error(`Class ${qualifiedName} not found in the application.`); + } -} +} diff --git a/src/types/jsonstream.d.ts b/src/types/jsonstream.d.ts new file mode 100644 index 0000000..78c4280 --- /dev/null +++ b/src/types/jsonstream.d.ts @@ -0,0 +1 @@ +declare module 'JSONStream'; \ No newline at end of file diff --git a/src/types/signale.d.ts b/src/types/signale.d.ts new file mode 100644 index 0000000..17680e4 --- /dev/null +++ b/src/types/signale.d.ts @@ -0,0 +1 @@ +declare module 'signale'; \ No newline at end of file diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..7754e4e --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1 @@ +export * from './logger'; \ No newline at end of file diff --git a/src/utils/logger.ts b/src/utils/logger.ts new file mode 100644 index 0000000..3bb6151 --- /dev/null +++ b/src/utils/logger.ts @@ -0,0 +1,45 @@ +// logger.ts +import { Signale } from 'signale'; +import chalk from 'chalk'; + +class Logger { + /** + * Instance of Signale for logging messages. + */ + + private signale: InstanceType; + + constructor(scope?: string) { + this.signale = new Signale({ scope }); + } + + info(...messages: unknown[]) { + this.signale.info(...messages.map(m => typeof m === 'string' ? chalk.cyan(m) : m)); + } + + success(...messages: unknown[]) { + this.signale.success(...messages.map(m => typeof m === 'string' ? chalk.greenBright(m) : m)); + } + + warn(...messages: unknown[]) { + this.signale.warn(...messages.map(m => typeof m === 'string' ? chalk.yellowBright(m) : m)); + } + + error(...messages: unknown[]) { + this.signale.error(...messages.map(m => typeof m === 'string' ? chalk.redBright.bold(m) : m)); + } + + debug(...messages: unknown[]) { + this.signale.debug(...messages.map(m => typeof m === 'string' ? chalk.magentaBright(m) : m)); + } + + prettyJson(title: string, obj: any) { + this.signale.info(chalk.blue.bold(title)); + this.signale.info(chalk.gray(JSON.stringify(obj, null, 2))); + } +} + +export const logger = new Logger(); +export function createLogger(scope: string) { + return new Logger(scope); +} diff --git a/test/conftest.ts b/test/conftest.ts index 7ce2d0b..349731f 100644 --- a/test/conftest.ts +++ b/test/conftest.ts @@ -20,10 +20,10 @@ import * as fs from 'fs'; import * as path from 'path'; -import * as https from 'https'; import extract from 'extract-zip'; -import {beforeAll, afterAll } from "bun:test"; +import { beforeAll, afterAll } from "bun:test"; import { CLDK } from "../src/CLDK"; +import { JavaAnalysis } from "../src/analysis/java/JavaAnalysis"; /* * Set up sample applications for testing @@ -36,7 +36,7 @@ beforeAll(async () => { const javaSampleAppsDir = path.join(__dirname, "test-applications", "java"); const appZipFile = path.join(javaSampleAppsDir, "sample.daytrader8-1.2.zip"); const extractedDir = path.join(javaSampleAppsDir, "sample.daytrader8-1.2"); - await extract(appZipFile, {dir: javaSampleAppsDir}); + await extract(appZipFile, { dir: javaSampleAppsDir }); /** * I am just hardcoding the extracted directory name for now. The extracted directory name would follow GitHub's @@ -63,7 +63,7 @@ beforeAll(async () => { */ afterAll(async () => { if (dayTraderApp) { - fs.rmSync(dayTraderApp, {recursive: true, force: true}); + fs.rmSync(dayTraderApp, { recursive: true, force: true }); } }) diff --git a/test/unit/analysis/java/JavaAnalysis.test.ts b/test/unit/analysis/java/JavaAnalysis.test.ts index b8c1844..59c6074 100644 --- a/test/unit/analysis/java/JavaAnalysis.test.ts +++ b/test/unit/analysis/java/JavaAnalysis.test.ts @@ -1,16 +1,94 @@ -import {daytraderJavaAnalysis} from "../../../conftest"; -import {expect, test } from "bun:test"; +import { JCallable, JCallableParameter, JType } from "../../../../src/models/java/"; +import { daytraderJavaAnalysis } from "../../../conftest"; +import { expect, test } from "bun:test"; +import { logger } from "../../../../src/utils"; -test("Must get analysis object from JavaAnalysis object", () => { - expect(daytraderJavaAnalysis).toBeDefined(); +test("Should get analysis object from JavaAnalysis object", () => { + expect(daytraderJavaAnalysis).toBeDefined(); }); -test("Must get JApplication instance", async () => { - const jApplication = await daytraderJavaAnalysis.getApplication(); - expect(jApplication).toBeDefined(); +test("Should get JApplication instance", async () => { + const jApplication = await daytraderJavaAnalysis.getApplication(); + expect(jApplication).toBeDefined(); }); -test("Must get Symbol Table", async () => { - const symbolTable = await daytraderJavaAnalysis.getSymbolTable(); - expect(symbolTable).toBeDefined(); +test("Should get Symbol Table", async () => { + const symbolTable = await daytraderJavaAnalysis.getSymbolTable(); + expect(symbolTable).toBeDefined(); +}); + +test("Should get all classes in a Java application", async () => { + await expect(daytraderJavaAnalysis.getAllClasses()).toBeDefined(); +}); + +test("Should get a specific class the application", async () => { + const tradeDirectObject = await daytraderJavaAnalysis.getClassByQualifiedName("com.ibm.websphere.samples.daytrader.impl.direct.TradeDirect"); + expect(async () => JType.parse(tradeDirectObject)).not.toThrow(); +}); + +test("Should throw error when a requested class in the application does not exist", async () => { + /** + * Quick note to self: There is a subtle difference between await expect(...) and expect(await ...) + * When there is an error, the reject happens even before the expect can be honored. So instead, we await the expect + * by saying "Hey, I expect this promise to be rejected with this error ..." + */ + await expect(daytraderJavaAnalysis.getClassByQualifiedName("this.class.does.not.Exist")).rejects.toThrow( + "Class this.class.does.not.Exist not found in the application."); +}); + +test("Should get all methods in the application", () => { + return daytraderJavaAnalysis.getAllMethods().then((methods) => { + expect(methods).toBeDefined() + }); +}); + +test("Should get all methods in a specific class in the application", async () => { + expect( + ( + await daytraderJavaAnalysis.getAllMethodsByClass("com.ibm.websphere.samples.daytrader.impl.direct.TradeDirect")).length + ).toBeGreaterThan(0) +}); + +test("Should get a specific method in a specific class in the application", async () => { + const method = await daytraderJavaAnalysis.getMethodByQualifiedName( + "com.ibm.websphere.samples.daytrader.impl.direct.TradeDirect", "publishQuotePriceChange(QuoteDataBean, BigDecimal, BigDecimal, double)"); + + expect(async () => JCallable.parse(method)).not.toThrow(); +}); + +test("Should get parameters of a specific method in a specific class in the application", async () => { + const parameters = await daytraderJavaAnalysis.getMethodParameters( + "com.ibm.websphere.samples.daytrader.impl.direct.TradeDirect", "publishQuotePriceChange(QuoteDataBean, BigDecimal, BigDecimal, double)"); + + expect(parameters).toBeDefined(); + logger.success("parameters are defined"); + expect(parameters.length).toBe(4); + logger.success("there are 4 parameters"); + parameters.forEach(param => { + expect(async () => JCallableParameter.parse(param)).not.toThrow(); + }); + logger.success("All parameters are valid JCallableParameter instances"); +}); + +test("Should get parameters of a specific method in a specific class in the application given the callable object", async () => { + const method = await daytraderJavaAnalysis.getMethodByQualifiedName( + "com.ibm.websphere.samples.daytrader.impl.direct.TradeDirect", "publishQuotePriceChange(QuoteDataBean, BigDecimal, BigDecimal, double)"); + const parameters = await daytraderJavaAnalysis.getMethodParametersFromCallable(method); + expect(parameters).toBeDefined(); + logger.success("parameters are defined"); + expect(parameters.length).toBe(4); + logger.success("there are 4 parameters"); + parameters.forEach(param => { + expect(async () => JCallableParameter.parse(param)).not.toThrow(); + } + ); + logger.success("All parameters are valid JCallableParameter instances"); +}); + + +test("Should get file path for a specific class in the application", async () => { + const filePath = await daytraderJavaAnalysis.getJavaFilePathByQualifiedName("com.ibm.websphere.samples.daytrader.impl.direct.TradeDirect"); + expect(filePath).toBeDefined(); + expect(filePath).toContain( + "main/java/com/ibm/websphere/samples/daytrader/impl/direct/TradeDirect.java"); }); \ No newline at end of file diff --git a/test/unit/utils/loggers.test.ts b/test/unit/utils/loggers.test.ts new file mode 100644 index 0000000..9edbdcc --- /dev/null +++ b/test/unit/utils/loggers.test.ts @@ -0,0 +1,40 @@ +import { test, expect } from 'bun:test'; +import { logger, createLogger } from '../../../src/utils/logger'; + +test("Should create a logger instance", () => { + expect(logger).toBeDefined(); +}); + +test("Should log info message without throwing", () => { + expect(() => logger.info("This is an info message")).not.toThrow(); +}); + +test("Should log success message without throwing", () => { + expect(() => logger.success("This is a success message")).not.toThrow(); +}); + +test("Should log warning message without throwing", () => { + expect(() => logger.warn("This is a warning message")).not.toThrow(); +}); + +test("Should log error message without throwing", () => { + expect(() => logger.error("This is an error message")).not.toThrow(); +}); + +test("Should log debug message without throwing", () => { + expect(() => logger.debug("This is a debug message")).not.toThrow(); +}); + +test("Should pretty print JSON without throwing", () => { + expect(() => logger.prettyJson("Test Object", { foo: "bar", baz: 42 })).not.toThrow(); +}); + +test("Should create scoped logger instance", () => { + const scopedLogger = createLogger("TestScope"); + expect(scopedLogger).toBeDefined(); +}); + +test("Scoped logger must log info without throwing", () => { + const scopedLogger = createLogger("TestScope"); + expect(() => scopedLogger.info("Scoped info message")).not.toThrow(); +}); diff --git a/tsconfig.json b/tsconfig.json index dfb54a2..281c75f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -28,10 +28,16 @@ "noPropertyAccessFromIndexSignature": false, "baseUrl": ".", "paths": { - "cldk": ["node_modules/@cldk/cldk"] - } + "cldk": [ + "node_modules/@cldk/cldk" + ] + }, + "typeRoots": [ + "./src/ypes", + "./node_modules/@types" + ] }, "include": [ "src" ] -} +} \ No newline at end of file