From c6cc3bb1248e4abe4fd8e007fa2f6c8eee84d88c Mon Sep 17 00:00:00 2001 From: Rahul Krishna Date: Sat, 26 Apr 2025 19:23:35 -0400 Subject: [PATCH 01/13] Add getAllClasses API and corresponding test cases. Signed-off-by: Rahul Krishna --- .../devcontainer.json => .devcontainer.json | 14 ++++--------- src/analysis/java/JavaAnalysis.ts | 21 ++++++++++++------- src/types/jsonstream.d.ts | 1 + test/unit/analysis/java/JavaAnalysis.test.ts | 6 +++++- 4 files changed, 23 insertions(+), 19 deletions(-) rename .devcontainer/devcontainer.json => .devcontainer.json (59%) create mode 100644 src/types/jsonstream.d.ts 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/src/analysis/java/JavaAnalysis.ts b/src/analysis/java/JavaAnalysis.ts index 3806fb2..0a364d0 100644 --- a/src/analysis/java/JavaAnalysis.ts +++ b/src/analysis/java/JavaAnalysis.ts @@ -23,6 +23,7 @@ import { JApplication } from "../../models/java"; import * as types from "../../models/java/types"; import os from "os"; import JSONStream from "JSONStream"; +declare module "JSONStream"; import crypto from "crypto"; enum AnalysisLevel { @@ -147,15 +148,19 @@ export class JavaAnalysis { return (await this.getApplication()).symbol_table; } - 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(); - } - + /** + * + */ + 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); } + public async getAllMethods(): Promise + } 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/test/unit/analysis/java/JavaAnalysis.test.ts b/test/unit/analysis/java/JavaAnalysis.test.ts index b8c1844..0af2e92 100644 --- a/test/unit/analysis/java/JavaAnalysis.test.ts +++ b/test/unit/analysis/java/JavaAnalysis.test.ts @@ -13,4 +13,8 @@ test("Must get JApplication instance", async () => { test("Must get Symbol Table", async () => { const symbolTable = await daytraderJavaAnalysis.getSymbolTable(); expect(symbolTable).toBeDefined(); -}); \ No newline at end of file +}); + +test("Must get all classes in a Java application", async () => { + expect(await daytraderJavaAnalysis.getAllClasses()).toBeDefined(); +}) \ No newline at end of file From 314185d5570e1938d18cdf374ffcab2678c69909 Mon Sep 17 00:00:00 2001 From: Rahul Krishna Date: Sat, 26 Apr 2025 20:12:32 -0400 Subject: [PATCH 02/13] Attach project to coveralls and generate coverage reports on push/pr/release with badge. Signed-off-by: Rahul Krishna --- .github/workflows/coverage.yml | 26 ++ .github/workflows/release.yml | 3 + .prettierrc | 3 + README.md | 55 ++-- bunfig.toml | 5 - package.json | 4 +- src/CLDK.ts | 108 ++++---- src/analysis/java/JavaAnalysis.ts | 266 ++++++++++--------- test/unit/analysis/java/JavaAnalysis.test.ts | 23 +- 9 files changed, 278 insertions(+), 215 deletions(-) create mode 100644 .github/workflows/coverage.yml delete mode 100644 bunfig.toml diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 0000000..7a4852b --- /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 + + - 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..5954545 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "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 --coverage --coverage-reporter=lcov --preload ./test/conftest.ts --timeout=600000 --verbose", "clean": "rm -rf dist coverage *.lock" }, "files": [ @@ -47,4 +47,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 0a364d0..a0a295b 100644 --- a/src/analysis/java/JavaAnalysis.ts +++ b/src/analysis/java/JavaAnalysis.ts @@ -21,146 +21,162 @@ import log from "loglevel"; import { spawnSync } from "node:child_process"; import { JApplication } from "../../models/java"; import * as types from "../../models/java/types"; +import { JType } from "../../models/java"; import os from "os"; import JSONStream from "JSONStream"; declare module "JSONStream"; import crypto from "crypto"; 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; - - 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]; - - 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]; + 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; + } + + 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."); } - - /** - * 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); - } + log.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(), + "-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)); }); - } - - /** - * 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; - } - public async getSymbolTable(): Promise> { - return (await this.getApplication()).symbol_table; - } + 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); + }); - /** - * - */ - 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); + 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(); } - - public async getAllMethods(): Promise - + return this.application; + } + + 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); + } + + public async getAllMethods(): Promise>> { + return Object.entries(await this.getAllClasses()).reduce((allMethods, [key, value]) => { + allMethods[key] = value.callable_declarations; + return allMethods; + }, {} as Record>); + } } - diff --git a/test/unit/analysis/java/JavaAnalysis.test.ts b/test/unit/analysis/java/JavaAnalysis.test.ts index 0af2e92..03c4cec 100644 --- a/test/unit/analysis/java/JavaAnalysis.test.ts +++ b/test/unit/analysis/java/JavaAnalysis.test.ts @@ -1,20 +1,25 @@ -import {daytraderJavaAnalysis} from "../../../conftest"; -import {expect, test } from "bun:test"; +import { daytraderJavaAnalysis } from "../../../conftest"; +import { expect, test } from "bun:test"; test("Must get analysis object from JavaAnalysis object", () => { - expect(daytraderJavaAnalysis).toBeDefined(); + expect(daytraderJavaAnalysis).toBeDefined(); }); test("Must get JApplication instance", async () => { - const jApplication = await daytraderJavaAnalysis.getApplication(); - expect(jApplication).toBeDefined(); + const jApplication = await daytraderJavaAnalysis.getApplication(); + expect(jApplication).toBeDefined(); }); test("Must get Symbol Table", async () => { - const symbolTable = await daytraderJavaAnalysis.getSymbolTable(); - expect(symbolTable).toBeDefined(); + const symbolTable = await daytraderJavaAnalysis.getSymbolTable(); + expect(symbolTable).toBeDefined(); }); test("Must get all classes in a Java application", async () => { - expect(await daytraderJavaAnalysis.getAllClasses()).toBeDefined(); -}) \ No newline at end of file + expect(await daytraderJavaAnalysis.getAllClasses()).toBeDefined(); +}); + +test("Must get all methods in the application", async () => { + const allMethods = await daytraderJavaAnalysis.getAllMethods(); + expect(allMethods).toBeDefined(); +}); From 00ca701a54a4d0b4c0edd12d8cefe4f83546ad40 Mon Sep 17 00:00:00 2001 From: Rahul Krishna Date: Sat, 26 Apr 2025 20:39:04 -0400 Subject: [PATCH 03/13] Add API to get a specific class in the application. Signed-off-by: Rahul Krishna --- src/analysis/java/JavaAnalysis.ts | 19 +++++++++++++++++++ test/unit/analysis/java/JavaAnalysis.test.ts | 15 ++++++++++++--- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/analysis/java/JavaAnalysis.ts b/src/analysis/java/JavaAnalysis.ts index a0a295b..da8a9bb 100644 --- a/src/analysis/java/JavaAnalysis.ts +++ b/src/analysis/java/JavaAnalysis.ts @@ -173,10 +173,29 @@ export class JavaAnalysis { }, {} as Record); } + /** + * 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 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.`); + } } diff --git a/test/unit/analysis/java/JavaAnalysis.test.ts b/test/unit/analysis/java/JavaAnalysis.test.ts index 03c4cec..467c50c 100644 --- a/test/unit/analysis/java/JavaAnalysis.test.ts +++ b/test/unit/analysis/java/JavaAnalysis.test.ts @@ -1,3 +1,4 @@ +import { JType } from "../../../../src/models/java/"; import { daytraderJavaAnalysis } from "../../../conftest"; import { expect, test } from "bun:test"; @@ -19,7 +20,15 @@ test("Must get all classes in a Java application", async () => { expect(await daytraderJavaAnalysis.getAllClasses()).toBeDefined(); }); -test("Must get all methods in the application", async () => { - const allMethods = await daytraderJavaAnalysis.getAllMethods(); - expect(allMethods).toBeDefined(); +test("Must get a specific class the application", async () => { + const tradeDirectObject = await daytraderJavaAnalysis.getClassByQualifiedName("com.ibm.websphere.samples.daytrader.impl.direct.TradeDirect"); + console.log(tradeDirectObject); + expect(async () => JType.parse(tradeDirectObject)).not.toThrow(); }); + +test("Must get all methods in the application", () => { + return daytraderJavaAnalysis.getAllMethods().then((methods) => { + expect(methods).toBeDefined() + }); +}); + From da60a60f6c8b6dc2dc6d9de78bd545fea66120cc Mon Sep 17 00:00:00 2001 From: Rahul Krishna Date: Sat, 26 Apr 2025 20:49:30 -0400 Subject: [PATCH 04/13] Add API to get a specific class in the application. Signed-off-by: Rahul Krishna --- package.json | 2 +- test/unit/analysis/java/JavaAnalysis.test.ts | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 5954545..91cde83 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "homepage": "https://github.com/codellm-devkit/typescript-sdk#readme", "scripts": { "build": "bun build ./src/index.ts --outdir ./dist", - "test": "bun test --coverage --coverage-reporter=lcov --preload ./test/conftest.ts --timeout=600000 --verbose", + "test": "bun test --verbose --coverage --coverage-reporter=lcov --preload ./test/conftest.ts --timeout=600000", "clean": "rm -rf dist coverage *.lock" }, "files": [ diff --git a/test/unit/analysis/java/JavaAnalysis.test.ts b/test/unit/analysis/java/JavaAnalysis.test.ts index 467c50c..f61bf7f 100644 --- a/test/unit/analysis/java/JavaAnalysis.test.ts +++ b/test/unit/analysis/java/JavaAnalysis.test.ts @@ -17,15 +17,24 @@ test("Must get Symbol Table", async () => { }); test("Must get all classes in a Java application", async () => { - expect(await daytraderJavaAnalysis.getAllClasses()).toBeDefined(); + await expect(daytraderJavaAnalysis.getAllClasses()).toBeDefined(); }); test("Must get a specific class the application", async () => { const tradeDirectObject = await daytraderJavaAnalysis.getClassByQualifiedName("com.ibm.websphere.samples.daytrader.impl.direct.TradeDirect"); - console.log(tradeDirectObject); expect(async () => JType.parse(tradeDirectObject)).not.toThrow(); }); +test("Must 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("Must get all methods in the application", () => { return daytraderJavaAnalysis.getAllMethods().then((methods) => { expect(methods).toBeDefined() From dcbbbbe664374bf98eab879967f51f58f60c9e8c Mon Sep 17 00:00:00 2001 From: Rahul Krishna Date: Sat, 26 Apr 2025 21:23:11 -0400 Subject: [PATCH 05/13] Add API to get all methods within a specific class in the application. Signed-off-by: Rahul Krishna --- .github/workflows/coverage.yml | 2 +- package.json | 4 +- src/analysis/java/JavaAnalysis.ts | 56 ++++++++++++++------ test/unit/analysis/java/JavaAnalysis.test.ts | 10 +++- 4 files changed, 52 insertions(+), 20 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 7a4852b..42a77bd 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -20,7 +20,7 @@ jobs: run: bun install - name: Run tests with coverage - run: bun run test + run: bun run test:withCoverage - name: Upload coverage to Coveralls uses: coverallsapp/github-action@v2 diff --git a/package.json b/package.json index 91cde83..6268b0d 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 --verbose --coverage --coverage-reporter=lcov --preload ./test/conftest.ts --timeout=600000", + "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,6 +37,7 @@ "@types/jsonstream": "^0.8.33", "JSONStream": "^1.3.5", "bun": "^1.2.10", + "c8": "^10.1.3", "extract-zip": "^2.0.1", "fast-glob": "^3.3.3", "graphology": "^0.26.0", diff --git a/src/analysis/java/JavaAnalysis.ts b/src/analysis/java/JavaAnalysis.ts index da8a9bb..1faa699 100644 --- a/src/analysis/java/JavaAnalysis.ts +++ b/src/analysis/java/JavaAnalysis.ts @@ -80,9 +80,9 @@ export class JavaAnalysis { const tmpFilePath = path.join(os.tmpdir(), `${Date.now()}-${crypto.randomUUID()}`); const command = [ ...this.getCodeAnalyzerExec(), - "-i", + "--input", projectPath, - "-o", + "--output", tmpFilePath, `--analysis-level=${this.analysisLevel}`, "--verbose", @@ -174,22 +174,14 @@ export class JavaAnalysis { } /** - * Get all methods in the application. - * @returns {Promise>>} A promise that resolves to a record of - * method names and their corresponding {@link JCallableType} objects + * 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 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. + * @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 getAllMethods(): Promise>> { - return Object.entries(await this.getAllClasses()).reduce((allMethods, [key, value]) => { - allMethods[key] = value.callable_declarations; - return allMethods; - }, {} as Record>); - } - public async getClassByQualifiedName(qualifiedName: string): Promise { const allClasses = await this.getAllClasses(); if (allClasses[qualifiedName]) { @@ -198,4 +190,36 @@ export class JavaAnalysis { 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>); + } + + /** + * 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 ?? {}) : []; + } } diff --git a/test/unit/analysis/java/JavaAnalysis.test.ts b/test/unit/analysis/java/JavaAnalysis.test.ts index f61bf7f..a32acf5 100644 --- a/test/unit/analysis/java/JavaAnalysis.test.ts +++ b/test/unit/analysis/java/JavaAnalysis.test.ts @@ -22,7 +22,7 @@ test("Must get all classes in a Java application", async () => { test("Must 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(); + expect(async () => JType.parse(tradeDirectObject)).not.toThrow(); }); test("Must throw error when a requested class in the application does not exist", async () => { @@ -32,7 +32,7 @@ test("Must throw error when a requested class in the application does not exist" * 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."); + "Class this.class.does.not.Exist not found in the application."); }); test("Must get all methods in the application", () => { @@ -41,3 +41,9 @@ test("Must get all methods in the application", () => { }); }); +test("Must 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) +}); \ No newline at end of file From cefedaaad8ed489b263b05c95464482625412064 Mon Sep 17 00:00:00 2001 From: Rahul Krishna Date: Sun, 27 Apr 2025 00:12:09 -0400 Subject: [PATCH 06/13] Add API to get a specific method given the qualified class name and the method signature. Signed-off-by: Rahul Krishna --- src/analysis/java/JavaAnalysis.ts | 28 +++++++++++++++++++- test/unit/analysis/java/JavaAnalysis.test.ts | 9 ++++++- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/analysis/java/JavaAnalysis.ts b/src/analysis/java/JavaAnalysis.ts index 1faa699..5b42eec 100644 --- a/src/analysis/java/JavaAnalysis.ts +++ b/src/analysis/java/JavaAnalysis.ts @@ -19,7 +19,7 @@ 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"; @@ -152,6 +152,15 @@ export class JavaAnalysis { 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; } @@ -222,4 +231,21 @@ export class JavaAnalysis { 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}.`); })(); + } } diff --git a/test/unit/analysis/java/JavaAnalysis.test.ts b/test/unit/analysis/java/JavaAnalysis.test.ts index a32acf5..0cd2ecb 100644 --- a/test/unit/analysis/java/JavaAnalysis.test.ts +++ b/test/unit/analysis/java/JavaAnalysis.test.ts @@ -1,4 +1,4 @@ -import { JType } from "../../../../src/models/java/"; +import { JCallable, JType } from "../../../../src/models/java/"; import { daytraderJavaAnalysis } from "../../../conftest"; import { expect, test } from "bun:test"; @@ -46,4 +46,11 @@ test("Must get all methods in a specific class in the application", async () => ( await daytraderJavaAnalysis.getAllMethodsByClass("com.ibm.websphere.samples.daytrader.impl.direct.TradeDirect")).length ).toBeGreaterThan(0) +}); + +test("Must 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(); }); \ No newline at end of file From ea064d8f42efb91054f814bb18a12790cdd62752 Mon Sep 17 00:00:00 2001 From: Rahul Krishna Date: Sun, 27 Apr 2025 00:17:29 -0400 Subject: [PATCH 07/13] Add API to get the array of method parameters given the qualified class name and the method signature. Signed-off-by: Rahul Krishna --- src/analysis/java/JavaAnalysis.ts | 14 ++++++++++++++ test/unit/analysis/java/JavaAnalysis.test.ts | 13 ++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/analysis/java/JavaAnalysis.ts b/src/analysis/java/JavaAnalysis.ts index 5b42eec..580e35b 100644 --- a/src/analysis/java/JavaAnalysis.ts +++ b/src/analysis/java/JavaAnalysis.ts @@ -248,4 +248,18 @@ export class JavaAnalysis { (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 ?? []; + } } diff --git a/test/unit/analysis/java/JavaAnalysis.test.ts b/test/unit/analysis/java/JavaAnalysis.test.ts index 0cd2ecb..f674444 100644 --- a/test/unit/analysis/java/JavaAnalysis.test.ts +++ b/test/unit/analysis/java/JavaAnalysis.test.ts @@ -1,4 +1,4 @@ -import { JCallable, JType } from "../../../../src/models/java/"; +import { JCallable, JCallableParameter, JType } from "../../../../src/models/java/"; import { daytraderJavaAnalysis } from "../../../conftest"; import { expect, test } from "bun:test"; @@ -53,4 +53,15 @@ test("Must get a specific method in a specific class in the application", async "com.ibm.websphere.samples.daytrader.impl.direct.TradeDirect", "publishQuotePriceChange(QuoteDataBean, BigDecimal, BigDecimal, double)"); expect(async () => JCallable.parse(method)).not.toThrow(); +}); + +test("Must 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(); + expect(parameters.length).toBeGreaterThan(0); + parameters.forEach(param => { + expect(async () => JCallableParameter.parse(param)).not.toThrow(); + }); }); \ No newline at end of file From 1ac335606e094497a54b6dd2446b1bcf1ea6ad7b Mon Sep 17 00:00:00 2001 From: Rahul Krishna Date: Mon, 28 Apr 2025 08:50:43 -0400 Subject: [PATCH 08/13] Add API to get the array of method parameters given the callable object. Signed-off-by: Rahul Krishna --- package.json | 2 ++ src/analysis/java/JavaAnalysis.ts | 32 ++++++++++++++++++++ test/conftest.ts | 7 ++--- test/unit/analysis/java/JavaAnalysis.test.ts | 27 +++++++++++++++-- 4 files changed, 62 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 6268b0d..cbfb87c 100644 --- a/package.json +++ b/package.json @@ -38,10 +38,12 @@ "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": { diff --git a/src/analysis/java/JavaAnalysis.ts b/src/analysis/java/JavaAnalysis.ts index 580e35b..87c9d89 100644 --- a/src/analysis/java/JavaAnalysis.ts +++ b/src/analysis/java/JavaAnalysis.ts @@ -262,4 +262,36 @@ export class JavaAnalysis { 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/test/conftest.ts b/test/conftest.ts index 7ce2d0b..7bfa99c 100644 --- a/test/conftest.ts +++ b/test/conftest.ts @@ -20,9 +20,8 @@ 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"; /* @@ -36,7 +35,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 +62,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 f674444..f973220 100644 --- a/test/unit/analysis/java/JavaAnalysis.test.ts +++ b/test/unit/analysis/java/JavaAnalysis.test.ts @@ -1,6 +1,10 @@ import { JCallable, JCallableParameter, JType } from "../../../../src/models/java/"; import { daytraderJavaAnalysis } from "../../../conftest"; import { expect, test } from "bun:test"; +import chalk from "chalk"; +import { Signale } from "signale"; + +const logger = new Signale(); test("Must get analysis object from JavaAnalysis object", () => { expect(daytraderJavaAnalysis).toBeDefined(); @@ -60,8 +64,27 @@ test("Must get parameters of a specific method in a specific class in the applic "com.ibm.websphere.samples.daytrader.impl.direct.TradeDirect", "publishQuotePriceChange(QuoteDataBean, BigDecimal, BigDecimal, double)"); expect(parameters).toBeDefined(); - expect(parameters.length).toBeGreaterThan(0); + logger.success(chalk.green("parameters are defined")); + expect(parameters.length).toBe(4); + logger.success(chalk.green("there are 4 parameters")); parameters.forEach(param => { expect(async () => JCallableParameter.parse(param)).not.toThrow(); }); -}); \ No newline at end of file + logger.success(chalk.green("All parameters are valid JCallableParameter instances")); +}); + +test("Must 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(chalk.green("parameters are defined")); + expect(parameters.length).toBe(4); + logger.success(chalk.green("there are 4 parameters")); + parameters.forEach(param => { + expect(async () => JCallableParameter.parse(param)).not.toThrow(); + } + ); + logger.success(chalk.green("All parameters are valid JCallableParameter instances")); +}); + From 9d6ffd5a128207828b8d02b4e30aa6046bb78437 Mon Sep 17 00:00:00 2001 From: Rahul Krishna Date: Mon, 28 Apr 2025 09:08:59 -0400 Subject: [PATCH 09/13] Add rich colorful logging Signed-off-by: Rahul Krishna --- src/analysis/java/JavaAnalysis.ts | 12 +++-- src/types/signale.d.ts | 1 + src/utils/index.ts | 1 + src/utils/logger.ts | 49 ++++++++++++++++++++ test/conftest.ts | 1 + test/unit/analysis/java/JavaAnalysis.test.ts | 17 +++---- tsconfig.json | 12 +++-- 7 files changed, 76 insertions(+), 17 deletions(-) create mode 100644 src/types/signale.d.ts create mode 100644 src/utils/index.ts create mode 100644 src/utils/logger.ts diff --git a/src/analysis/java/JavaAnalysis.ts b/src/analysis/java/JavaAnalysis.ts index 87c9d89..e581750 100644 --- a/src/analysis/java/JavaAnalysis.ts +++ b/src/analysis/java/JavaAnalysis.ts @@ -26,6 +26,9 @@ import os from "os"; import JSONStream from "JSONStream"; declare module "JSONStream"; import crypto from "crypto"; +import { createLogger } from "src/utils"; + +const logger = createLogger("JavaAnalysis"); enum AnalysisLevel { SYMBOL_TABLE = "1", @@ -56,10 +59,10 @@ export class JavaAnalysis { const jarPath = matches[0]; if (!jarPath) { - console.log("Default codeanalyzer jar not found."); + logger.error("Default codeanalyzer jar not found."); throw new Error("Default codeanalyzer jar not found."); } - log.info("Codeanalyzer jar found at:", jarPath); + logger.info("Codeanalyzer jar found at:", jarPath); return ["java", "-jar", jarPath]; } @@ -91,7 +94,7 @@ export class JavaAnalysis { if (!command[0]) { return reject(new Error("Codeanalyzer command not found")); } - log.debug(command.join(" ")); + logger.debug(command.join(" ")); const result = spawnSync(command[0], command.slice(1), { stdio: ["ignore", "pipe", "inherit"], }); @@ -115,8 +118,9 @@ export class JavaAnalysis { stream.on("end", () => { // Clean up the temporary file + logger.debug(`Deleting temporary file: ${tmpFilePath}`); fs.rm(tmpFilePath, { recursive: true, force: true }, (err) => { - if (err) log.warn(`Failed to delete temporary file: ${tmpFilePath}`, err); + if (err) logger.warn(`Failed to delete temporary file: ${tmpFilePath}`, err); }); resolve(result as types.JApplicationType); }); 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..82113ea --- /dev/null +++ b/src/utils/logger.ts @@ -0,0 +1,49 @@ +// logger.ts +import Signale from 'signale'; +import chalk from 'chalk'; + +class Logger { + /** + * Instance of Signale for logging messages. + */ + + /** + * @note Instead of using `private signale: Signale;`, I am using `InstanceType` to tell + * typescript, "Hey, signale is an instance of the {@link Signale} class." + */ + 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 7bfa99c..349731f 100644 --- a/test/conftest.ts +++ b/test/conftest.ts @@ -23,6 +23,7 @@ import * as path from 'path'; import extract from 'extract-zip'; import { beforeAll, afterAll } from "bun:test"; import { CLDK } from "../src/CLDK"; +import { JavaAnalysis } from "../src/analysis/java/JavaAnalysis"; /* * Set up sample applications for testing diff --git a/test/unit/analysis/java/JavaAnalysis.test.ts b/test/unit/analysis/java/JavaAnalysis.test.ts index f973220..93017c2 100644 --- a/test/unit/analysis/java/JavaAnalysis.test.ts +++ b/test/unit/analysis/java/JavaAnalysis.test.ts @@ -1,10 +1,7 @@ import { JCallable, JCallableParameter, JType } from "../../../../src/models/java/"; import { daytraderJavaAnalysis } from "../../../conftest"; import { expect, test } from "bun:test"; -import chalk from "chalk"; -import { Signale } from "signale"; - -const logger = new Signale(); +import { logger } from "../../../../src/utils"; test("Must get analysis object from JavaAnalysis object", () => { expect(daytraderJavaAnalysis).toBeDefined(); @@ -64,13 +61,13 @@ test("Must get parameters of a specific method in a specific class in the applic "com.ibm.websphere.samples.daytrader.impl.direct.TradeDirect", "publishQuotePriceChange(QuoteDataBean, BigDecimal, BigDecimal, double)"); expect(parameters).toBeDefined(); - logger.success(chalk.green("parameters are defined")); + logger.success("parameters are defined"); expect(parameters.length).toBe(4); - logger.success(chalk.green("there are 4 parameters")); + logger.success("there are 4 parameters"); parameters.forEach(param => { expect(async () => JCallableParameter.parse(param)).not.toThrow(); }); - logger.success(chalk.green("All parameters are valid JCallableParameter instances")); + logger.success("All parameters are valid JCallableParameter instances"); }); test("Must get parameters of a specific method in a specific class in the application given the callable object", async () => { @@ -78,13 +75,13 @@ test("Must get parameters of a specific method in a specific class in the applic "com.ibm.websphere.samples.daytrader.impl.direct.TradeDirect", "publishQuotePriceChange(QuoteDataBean, BigDecimal, BigDecimal, double)"); const parameters = await daytraderJavaAnalysis.getMethodParametersFromCallable(method); expect(parameters).toBeDefined(); - logger.success(chalk.green("parameters are defined")); + logger.success("parameters are defined"); expect(parameters.length).toBe(4); - logger.success(chalk.green("there are 4 parameters")); + logger.success("there are 4 parameters"); parameters.forEach(param => { expect(async () => JCallableParameter.parse(param)).not.toThrow(); } ); - logger.success(chalk.green("All parameters are valid JCallableParameter instances")); + logger.success("All parameters are valid JCallableParameter instances"); }); 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 From cc797b646ba1fc61f4c1361754377886fb7f5c55 Mon Sep 17 00:00:00 2001 From: Rahul Krishna Date: Mon, 28 Apr 2025 09:10:49 -0400 Subject: [PATCH 10/13] Add rich colorful logging Signed-off-by: Rahul Krishna --- src/utils/logger.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/utils/logger.ts b/src/utils/logger.ts index 82113ea..3bb6151 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -1,5 +1,5 @@ // logger.ts -import Signale from 'signale'; +import { Signale } from 'signale'; import chalk from 'chalk'; class Logger { @@ -7,10 +7,6 @@ class Logger { * Instance of Signale for logging messages. */ - /** - * @note Instead of using `private signale: Signale;`, I am using `InstanceType` to tell - * typescript, "Hey, signale is an instance of the {@link Signale} class." - */ private signale: InstanceType; constructor(scope?: string) { From e382132213b407bbdb54fc26828f5dcbf089c8cc Mon Sep 17 00:00:00 2001 From: Rahul Krishna Date: Mon, 28 Apr 2025 09:19:18 -0400 Subject: [PATCH 11/13] Add unit tests for all the logging methods I just wrote Signed-off-by: Rahul Krishna --- src/analysis/java/JavaAnalysis.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/analysis/java/JavaAnalysis.ts b/src/analysis/java/JavaAnalysis.ts index e581750..c635be1 100644 --- a/src/analysis/java/JavaAnalysis.ts +++ b/src/analysis/java/JavaAnalysis.ts @@ -17,7 +17,6 @@ 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, JCompilationUnit } from "../../models/java"; import * as types from "../../models/java/types"; From 8f93112adac4befc2f6becbe78e750dac67291f9 Mon Sep 17 00:00:00 2001 From: Rahul Krishna Date: Mon, 28 Apr 2025 09:21:25 -0400 Subject: [PATCH 12/13] Add unit tests for all the logging methods I just wrote Signed-off-by: Rahul Krishna --- test/unit/utils/loggers.test.ts | 40 +++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 test/unit/utils/loggers.test.ts diff --git a/test/unit/utils/loggers.test.ts b/test/unit/utils/loggers.test.ts new file mode 100644 index 0000000..caa5f52 --- /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("Must create a logger instance", () => { + expect(logger).toBeDefined(); +}); + +test("Must log info message without throwing", () => { + expect(() => logger.info("This is an info message")).not.toThrow(); +}); + +test("Must log success message without throwing", () => { + expect(() => logger.success("This is a success message")).not.toThrow(); +}); + +test("Must log warning message without throwing", () => { + expect(() => logger.warn("This is a warning message")).not.toThrow(); +}); + +test("Must log error message without throwing", () => { + expect(() => logger.error("This is an error message")).not.toThrow(); +}); + +test("Must log debug message without throwing", () => { + expect(() => logger.debug("This is a debug message")).not.toThrow(); +}); + +test("Must pretty print JSON without throwing", () => { + expect(() => logger.prettyJson("Test Object", { foo: "bar", baz: 42 })).not.toThrow(); +}); + +test("Must 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(); +}); From 16f8e9d203c5b85a4a616397cfbadd3410d95ec6 Mon Sep 17 00:00:00 2001 From: Rahul Krishna Date: Mon, 5 May 2025 14:36:03 -0400 Subject: [PATCH 13/13] Add get file path API Signed-off-by: Rahul Krishna --- src/analysis/java/JavaAnalysis.ts | 5 ++-- test/unit/analysis/java/JavaAnalysis.test.ts | 29 ++++++++++++-------- test/unit/utils/loggers.test.ts | 16 +++++------ 3 files changed, 29 insertions(+), 21 deletions(-) diff --git a/src/analysis/java/JavaAnalysis.ts b/src/analysis/java/JavaAnalysis.ts index c635be1..9fc8c1a 100644 --- a/src/analysis/java/JavaAnalysis.ts +++ b/src/analysis/java/JavaAnalysis.ts @@ -22,8 +22,6 @@ 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"; -declare module "JSONStream"; import crypto from "crypto"; import { createLogger } from "src/utils"; @@ -108,6 +106,7 @@ export class JavaAnalysis { // 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; @@ -297,4 +296,6 @@ export class JavaAnalysis { } throw new Error(`Class ${qualifiedName} not found in the application.`); } + + } diff --git a/test/unit/analysis/java/JavaAnalysis.test.ts b/test/unit/analysis/java/JavaAnalysis.test.ts index 93017c2..59c6074 100644 --- a/test/unit/analysis/java/JavaAnalysis.test.ts +++ b/test/unit/analysis/java/JavaAnalysis.test.ts @@ -3,30 +3,30 @@ import { daytraderJavaAnalysis } from "../../../conftest"; import { expect, test } from "bun:test"; import { logger } from "../../../../src/utils"; -test("Must get analysis object from JavaAnalysis object", () => { +test("Should get analysis object from JavaAnalysis object", () => { expect(daytraderJavaAnalysis).toBeDefined(); }); -test("Must get JApplication instance", async () => { +test("Should get JApplication instance", async () => { const jApplication = await daytraderJavaAnalysis.getApplication(); expect(jApplication).toBeDefined(); }); -test("Must get Symbol Table", async () => { +test("Should get Symbol Table", async () => { const symbolTable = await daytraderJavaAnalysis.getSymbolTable(); expect(symbolTable).toBeDefined(); }); -test("Must get all classes in a Java application", async () => { +test("Should get all classes in a Java application", async () => { await expect(daytraderJavaAnalysis.getAllClasses()).toBeDefined(); }); -test("Must get a specific class the application", async () => { +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("Must throw error when a requested class in the application does not exist", async () => { +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 @@ -36,27 +36,27 @@ test("Must throw error when a requested class in the application does not exist" "Class this.class.does.not.Exist not found in the application."); }); -test("Must get all methods in the application", () => { +test("Should get all methods in the application", () => { return daytraderJavaAnalysis.getAllMethods().then((methods) => { expect(methods).toBeDefined() }); }); -test("Must get all methods in a specific class in the application", async () => { +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("Must get a specific method in a specific class in the application", async () => { +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("Must get parameters of a specific method in a specific class in the application", async () => { +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)"); @@ -70,7 +70,7 @@ test("Must get parameters of a specific method in a specific class in the applic logger.success("All parameters are valid JCallableParameter instances"); }); -test("Must get parameters of a specific method in a specific class in the application given the callable object", async () => { +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); @@ -85,3 +85,10 @@ test("Must get parameters of a specific method in a specific class in the applic 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 index caa5f52..9edbdcc 100644 --- a/test/unit/utils/loggers.test.ts +++ b/test/unit/utils/loggers.test.ts @@ -1,35 +1,35 @@ import { test, expect } from 'bun:test'; import { logger, createLogger } from '../../../src/utils/logger'; -test("Must create a logger instance", () => { +test("Should create a logger instance", () => { expect(logger).toBeDefined(); }); -test("Must log info message without throwing", () => { +test("Should log info message without throwing", () => { expect(() => logger.info("This is an info message")).not.toThrow(); }); -test("Must log success message without throwing", () => { +test("Should log success message without throwing", () => { expect(() => logger.success("This is a success message")).not.toThrow(); }); -test("Must log warning message without throwing", () => { +test("Should log warning message without throwing", () => { expect(() => logger.warn("This is a warning message")).not.toThrow(); }); -test("Must log error message without throwing", () => { +test("Should log error message without throwing", () => { expect(() => logger.error("This is an error message")).not.toThrow(); }); -test("Must log debug message without throwing", () => { +test("Should log debug message without throwing", () => { expect(() => logger.debug("This is a debug message")).not.toThrow(); }); -test("Must pretty print JSON without throwing", () => { +test("Should pretty print JSON without throwing", () => { expect(() => logger.prettyJson("Test Object", { foo: "bar", baz: 42 })).not.toThrow(); }); -test("Must create scoped logger instance", () => { +test("Should create scoped logger instance", () => { const scopedLogger = createLogger("TestScope"); expect(scopedLogger).toBeDefined(); });