diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..e69de29 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f7e064d --- /dev/null +++ b/.gitignore @@ -0,0 +1,76 @@ +# Dependencies +node_modules/ + +# Lock files (for library development) +yarn.lock +package-lock.json +pnpm-lock.yaml +bun.lock +bun.lockb + +# Yarn Berry specific +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions + +# Bun +.bun + +# Volta +.volta/ + +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Cache and build directories +.cache/ +.next/ +.nuxt/ +dist/ +build/ +out/ +coverage/ +.turbo + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Editor directories and files +.idea/ +.vscode/ +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +.DS_Store +Thumbs.db + +# Testing +.nyc_output +coverage/ +*.lcov + +# TypeScript +*.tsbuildinfo +next-env.d.ts + +# Misc +.tmp/ +temp/ +.eslintcache +.stylelintcache \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md new file mode 100644 index 0000000..26c06e2 --- /dev/null +++ b/README.md @@ -0,0 +1,51 @@ +# Codellm-Devkit TypeScript SDK + +## 🚀 Overview +Codellm-Devkit (CLDK) is a multilingual program analysis framework that bridges the gap between traditional static analysis tools and Large Language Models (LLMs) specialized for code (CodeLLMs). Codellm-Devkit allows developers to streamline the process of transforming raw code into actionable insights by providing a unified interface for integrating outputs from various analysis tools and preparing them for effective use by CodeLLMs. + +## 📦 Installation + +To install the Codellm-Devkit TypeScript SDK, you can use npm or yarn. Run the following command in your terminal: + +### Using npm +```bash +npm install --save github:codellm-devkit/typescript-sdk#initial-sdk +``` + +### Using yarn +```bash +yarn add github:codellm-devkit/typescript-sdk#initial-sdk +``` +If you are on yarn v1 +```bash +yarn add codellm-devkit/typescript-sdk#initial-sdk +``` + +### Using bun +```bash +bun add github:codellm-devkit/typescript-sdk#initial-sdk +``` + +Then run `npm install`, `yarn install`, or `bun install` depending on your package manager. + +## ⚙️ Basic Usage + +Here’s how to use CLDK to analyze a Java project and access key analysis artifacts: + +```typescript +import { CLDK } from "cldk"; + +// Initialize Java analysis +const analysis = CLDK.for("java").analysis({ + 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); +``` diff --git a/bunfig.toml b/bunfig.toml new file mode 100644 index 0000000..723324d --- /dev/null +++ b/bunfig.toml @@ -0,0 +1,5 @@ +[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 new file mode 100644 index 0000000..1d26617 --- /dev/null +++ b/package.json @@ -0,0 +1,40 @@ +{ + "name": "cldk", + "version": "0.1.0", + "description": "", + "main": "dist/index.js", + "scripts": { + "build": "bun build ./src/index.ts --outdir ./dist", + "test": "bun test --preload ./test/conftest.ts --timeout=600000 --verbose", + "clean": "rm -rf dist coverage" + }, + "files": [ + "dist", + "src/analysis/java/jars/codeanalyzer-2.3.3.jar" + ], + "exports": [ + "./dist/index.js" + ], + "devDependencies": { + "@types/bun": "^1.2.10", + "@types/extract-zip": "2.0.0", + "@types/node": "^22.14.1", + "typescript": "^5.8.3" + }, + "private": true, + "dependencies": { + "@types/jsonstream": "^0.8.33", + "JSONStream": "^1.3.5", + "bun": "^1.2.10", + "extract-zip": "^2.0.1", + "fast-glob": "^3.3.3", + "graphology": "^0.26.0", + "loglevel": "^1.9.2", + "zod": "^3.24.3" + }, + "testing": { + "java-test-applications-path": "./test-applications/java", + "c-test-applications-path": "./test-applications/c", + "python-test-applications-path": "./test-applications/python" + } +} diff --git a/src/CLDK.ts b/src/CLDK.ts new file mode 100644 index 0000000..5e6c39b --- /dev/null +++ b/src/CLDK.ts @@ -0,0 +1,57 @@ +import {JavaAnalysis} from "./analysis/java"; +import {spawnSync} from "node:child_process"; + +export class CLDK { + /** + * The programming language of choice + */ + private language: string; + + 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); + } + + /** + * 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.`); + } + } + + 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/commons/treesitter/TreesitterJava.ts b/src/analysis/commons/treesitter/TreesitterJava.ts new file mode 100644 index 0000000..44787e8 --- /dev/null +++ b/src/analysis/commons/treesitter/TreesitterJava.ts @@ -0,0 +1,19 @@ +/** + * Copyright IBM Corporation 2025 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export class TreesitterJava { + +} \ No newline at end of file diff --git a/src/analysis/java/JavaAnalysis.ts b/src/analysis/java/JavaAnalysis.ts new file mode 100644 index 0000000..3806fb2 --- /dev/null +++ b/src/analysis/java/JavaAnalysis.ts @@ -0,0 +1,161 @@ +/** + * Copyright IBM Corporation 2025 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +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 * as types from "../../models/java/types"; +import os from "os"; +import JSONStream from "JSONStream"; +import crypto from "crypto"; + +enum AnalysisLevel { + 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 +}; + +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]; + } + + /** + * 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); + } + }); + } + + /** + * 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; + } + + 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(); + } + + } + +} + diff --git a/src/analysis/java/index.ts b/src/analysis/java/index.ts new file mode 100644 index 0000000..6576038 --- /dev/null +++ b/src/analysis/java/index.ts @@ -0,0 +1,17 @@ +/** + * Copyright IBM Corporation 2025 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './JavaAnalysis'; \ No newline at end of file diff --git a/src/analysis/java/jars/codeanalyzer-2.3.3.jar b/src/analysis/java/jars/codeanalyzer-2.3.3.jar new file mode 100644 index 0000000..38387d3 Binary files /dev/null and b/src/analysis/java/jars/codeanalyzer-2.3.3.jar differ diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..c0539ac --- /dev/null +++ b/src/index.ts @@ -0,0 +1 @@ +export * from './CLDK'; \ No newline at end of file diff --git a/src/models/java/builders.ts b/src/models/java/builders.ts new file mode 100644 index 0000000..7bb6ce1 --- /dev/null +++ b/src/models/java/builders.ts @@ -0,0 +1,57 @@ +import {addToLookupTable, getFromLookupTable} from "./lookupTable"; + +export function buildMethodDetail(value) { + const {type_declaration, signature, callable_declaration} = value; +// Look up or create a new JCallable + let j_callable = getFromLookupTable(type_declaration, signature); + + if (!j_callable) { + // Create parameters from the callable declaration + const parameterString = callable_declaration.split('(')[1]?.split(')')[0] || ''; + const parameters = parameterString === '' + ? [] + : parameterString.split(',').map(t => ({ + name: null, + type: t.trim(), + annotations: [], + modifiers: [], + start_line: -1, + end_line: -1, + start_column: -1, + end_column: -1 + })); + + // Create a new JCallable + j_callable = { + signature, + is_implicit: true, + is_constructor: callable_declaration.includes(""), + comments: [], + annotations: [], + modifiers: [], + thrown_exceptions: [], + declaration: "", + parameters, + code: "", + start_line: -1, + end_line: -1, + referenced_types: [], + accessed_fields: [], + call_sites: [], + variable_declarations: [], + crud_operations: null, + crud_queries: null, + cyclomatic_complexity: 0 + }; + + // Store in lookup table + addToLookupTable(type_declaration, signature, j_callable); + } + + // Create and return JMethodDetail + return { + method_declaration: j_callable.declaration, + klass: type_declaration, + method: j_callable + }; +} \ No newline at end of file diff --git a/src/models/java/enums.ts b/src/models/java/enums.ts new file mode 100644 index 0000000..3d4c6e8 --- /dev/null +++ b/src/models/java/enums.ts @@ -0,0 +1,40 @@ +/** + * Copyright IBM Corporation 2025 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import z from 'zod'; + +/** + * Enum for CRUD query types. + * + * @typedef {Object} CRUDQueryType + * @enum {string} + * @property {string} READ - Represents a read query. + * @property {string} WRITE - Represents a write query. + * @property {string} NAMED - Represents a named query. + */ +export const CRUDQueryType = z.enum(['READ', 'WRITE', 'NAMED']); + +/** + * Enum for CRUD operation types. + * + * @typedef {Object} CRUDOperationType + * @enum {string} + * @property {string} CREATE - Represents a create operation. + * @property {string} READ - Represents a read operation. + * @property {string} UPDATE - Represents an update operation. + * @property {string} DELETE - Represents a delete operation. + */ +export const CRUDOperationType = z.enum(['CREATE', 'READ', 'UPDATE', 'DELETE']); diff --git a/src/models/java/index.ts b/src/models/java/index.ts new file mode 100644 index 0000000..5d5aa13 --- /dev/null +++ b/src/models/java/index.ts @@ -0,0 +1 @@ +export * from './schema'; \ No newline at end of file diff --git a/src/models/java/lookupTable.ts b/src/models/java/lookupTable.ts new file mode 100644 index 0000000..3c62650 --- /dev/null +++ b/src/models/java/lookupTable.ts @@ -0,0 +1,32 @@ +/** + * Global lookup table for JCallable instances + * @type {Map} + */ +const _CALLABLES_LOOKUP_TABLE = new Map(); + +/** + * Add an entry to the lookup table + * @param {string} typeName - The type name + * @param {string} signature - The method signature + * @param {Object} callable - The JCallable object + */ +export function addToLookupTable(typeName, signature, callable) { + _CALLABLES_LOOKUP_TABLE.set(`${typeName}:${signature}`, callable); +} + +/** + * Get an entry from the lookup table + * @param {string} typeName - The type name + * @param {string} signature - The method signature + * @return {Object} - The JCallable object + */ +export function getFromLookupTable(typeName, signature) { + return _CALLABLES_LOOKUP_TABLE.get(`${typeName}:${signature}`); +} + +/** + * Build a method detail from a value in the Lookup table + * @param {string} typeName - The type name + * @param {string} signature - The method signature + * @returns {Object} - The JCallable object + */ diff --git a/src/models/java/schema.ts b/src/models/java/schema.ts new file mode 100644 index 0000000..4997dfb --- /dev/null +++ b/src/models/java/schema.ts @@ -0,0 +1,421 @@ +/** + * Copyright IBM Corporation 2025 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import z from 'zod'; +import {CRUDOperationType, CRUDQueryType} from './enums'; +import {buildMethodDetail} from './builders'; +import {addToLookupTable} from "./lookupTable"; + +/** + * Represents a comment in Java source code. + * + * @typedef {Object} JComment + * @property {string|null|undefined} content - The content of the comment. + * @property {number} start_line - The starting line of the comment. + * @property {number} end_line - The ending line of the comment. + * @property {number} start_column - The starting column of the comment. + * @property {number} end_column - The ending column of the comment. + * @property {boolean} is_javadoc - Whether the comment is a Javadoc comment. + */ +export const JComment = z.object({ + content: z.string().nullable().optional(), + start_line: z.number().default(-1), + end_line: z.number().default(-1), + start_column: z.number().default(-1), + end_column: z.number().default(-1), + is_javadoc: z.boolean().default(false) +}); + +/** + * Represents a record component in Java. + * + * @typedef {Object} JRecordComponent + * @property {JComment} comment - The comment associated with the record component. + * @property {string} name - The name of the record component. + * @property {string} type - The type of the record component. + * @property {string[]} modifiers - The modifiers of the record component. + * @property {string[]} annotations - The annotations of the record component. + * @property {string|any|null} default_value - The default value of the record component. + * @property {boolean} is_var_args - Whether the record component is a varargs parameter. + */ +export const JRecordComponent = z.object({ + comment: JComment.nullable().optional(), + name: z.string(), + type: z.string(), + modifiers: z.array(z.string()), + annotations: z.array(z.string()), + default_value: z.union([z.string(), z.any(), z.null()]), + is_var_args: z.boolean() +}); + +/** + * Represents a field in Java. + * + * @typedef {Object} JField + * @property {JComment|undefined} comment - The comment associated with the field. + * @property {string} type - The type of the field. + * @property {number} start_line - The starting line of the field. + * @property {number} end_line - The ending line of the field. + * @property {string[]} variables - The variables declared in the field. + * @property {string[]} modifiers - The modifiers of the field. + * @property {string[]} annotations - The annotations of the field. + */ +export const JField = z.object({ + comment: JComment.optional(), + type: z.string(), + start_line: z.number(), + end_line: z.number(), + variables: z.array(z.string()), + modifiers: z.array(z.string()), + annotations: z.array(z.string()) +}); + +/** + * Represents a callable parameter in Java. + * + * @typedef {Object} JCallableParameter + * @property {string|null|undefined} name - The name of the parameter. + * @property {string} type - The type of the parameter. + * @property {string[]} annotations - The annotations of the parameter. + * @property {string[]} modifiers - The modifiers of the parameter. + * @property {number} start_line - The starting line of the parameter. + * @property {number} end_line - The ending line of the parameter. + * @property {number} start_column - The starting column of the parameter. + * @property {number} end_column - The ending column of the parameter. + */ +export const JCallableParameter = z.object({ + name: z.string().nullable().optional(), + type: z.string(), + annotations: z.array(z.string()), + modifiers: z.array(z.string()), + start_line: z.number(), + end_line: z.number(), + start_column: z.number(), + end_column: z.number() +}); + +/** + * Represents an enum constant in Java. + * + * @typedef {Object} JEnumConstant + * @property {string} name - The name of the enum constant. + * @property {string[]} arguments - The arguments passed to the enum constant. + */ +export const JEnumConstant = z.object({ + name: z.string(), + arguments: z.array(z.string()) +}); + +/** + * Represents a CRUD operation in Java. + * + * @typedef {Object} JCRUDOperation + * @property {number} line_number - The line number where the operation occurs. + * @property {CRUDOperationType|null|undefined} operation_type - The type of the CRUD operation. + */ +export const JCRUDOperation = z.object({ + line_number: z.number(), + operation_type: CRUDOperationType.optional().nullable() +}); + +/** + * Represents a CRUD query in Java. + * + * @typedef {Object} JCRUDQuery + * @property {number} line_number - The line number where the query occurs. + * @property {string[]|null|undefined} query_arguments - The arguments passed to the query. + * @property {CRUDQueryType|null|undefined} query_type - The type of the CRUD query. + */ +export const JCRUDQuery = z.object({ + line_number: z.number(), + query_arguments: z.array(z.string()).optional().nullable(), + query_type: CRUDQueryType.optional().nullable() +}); + +/** + * Represents a call site in Java code. + * + * @typedef {Object} JCallSite + * @property {JComment|null|undefined} comment - The comment associated with the call site. + * @property {string} method_name - The name of the method called at the call site. + * @property {string} receiver_expr - Expression for the receiver of the method call. + * @property {string} receiver_type - Name of type declaring the called method. + * @property {string[]} argument_types - Types of actual parameters for the call. + * @property {string} return_type - Return type of the method call (resolved type of the method call expression; empty string if expression is unresolved). + * @property {string} callee_signature - Signature of the callee. + * @property {boolean|null} is_static_call - Flag indicating whether the call is a static call. + * @property {boolean|null} is_private - Flag indicating whether the call is a private call. + * @property {boolean|null} is_public - Flag indicating whether the call is a public call. + * @property {boolean|null} is_protected - Flag indicating whether the call is a protected call. + * @property {boolean|null} is_unspecified - Flag indicating whether the call is an unspecified call. + * @property {boolean} is_constructor_call - Flag indicating whether the call is a constructor call. + * @property {JCRUDOperation|null} crud_operation - The CRUD operation type of the call site. + * @property {JCRUDQuery|null} crud_query - The CRUD query type of the call site. + * @property {number} start_line - The starting line number of the call site. + * @property {number} start_column - The starting column of the call site. + * @property {number} end_line - The ending line number of the call site. + * @property {number} end_column - The ending column of the call site. + */ +export const JCallSite = z.object({ + comment: JComment.nullable().optional(), + method_name: z.string(), + receiver_expr: z.string().default(""), + receiver_type: z.string(), + argument_types: z.array(z.string()), + return_type: z.string().default(""), + callee_signature: z.string().default(""), + is_static_call: z.boolean().nullable(), + is_private: z.boolean().nullable(), + is_public: z.boolean().nullable(), + is_protected: z.boolean().nullable(), + is_unspecified: z.boolean().nullable(), + is_constructor_call: z.boolean(), + crud_operation: z.union([JCRUDOperation, z.null()]), + crud_query: z.union([JCRUDQuery, z.null()]), + start_line: z.number(), + start_column: z.number(), + end_line: z.number(), + end_column: z.number() +}); + +/** + * Represents a variable declaration in Java. + * + * @typedef {Object} JVariableDeclaration + * @property {JComment|null} comment - The comment associated with the variable declaration. + * @property {string} name - The name of the variable. + * @property {string} type - The type of the variable. + * @property {string} initializer - The initialization expression (if present) for the variable declaration. + * @property {number} start_line - The starting line number of the declaration. + * @property {number} start_column - The starting column of the declaration. + * @property {number} end_line - The ending line number of the declaration. + * @property {number} end_column - The ending column of the declaration. + */ +export const JVariableDeclaration = z.object({ + comment: JComment.nullable().optional(), + name: z.string(), + type: z.string(), + initializer: z.string(), + start_line: z.number(), + start_column: z.number(), + end_line: z.number(), + end_column: z.number() +}); + +/** + * Represents an initialization block in Java. + * + * @typedef {Object} InitializationBlock + * @property {string} file_path - The path to the source file. + * @property {JComment[]} comments - The comments associated with the block. + * @property {string[]} annotations - The annotations applied to the block. + * @property {string[]} thrown_exceptions - Exceptions declared via "throws". + * @property {string} code - The code block. + * @property {number} start_line - The starting line number of the block in the source file. + * @property {number} end_line - The ending line number of the block in the source file. + * @property {boolean} is_static - A flag indicating whether the block is static. + * @property {string[]} referenced_types - The types referenced within the block. + * @property {string[]} accessed_fields - Fields accessed in the block. + * @property {JCallSite[]} call_sites - Call sites in the block. + * @property {JVariableDeclaration[]} variable_declarations - Local variable declarations in the block. + * @property {number} cyclomatic_complexity - Cyclomatic complexity of the block. + */ +export const InitializationBlock = z.object({ + file_path: z.string(), + comments: z.array(JComment), + annotations: z.array(z.string()), + thrown_exceptions: z.array(z.string()), + code: z.string(), + start_line: z.number(), + end_line: z.number(), + is_static: z.boolean(), + referenced_types: z.array(z.string()), + accessed_fields: z.array(z.string()), + call_sites: z.array(JCallSite), + variable_declarations: z.array(JVariableDeclaration), + cyclomatic_complexity: z.number() +}); + +/** + * Represents a callable entity such as a method or constructor in Java. + * + * @typedef {Object} JCallable + * @property {string} signature - The signature of the callable. + * @property {boolean} is_implicit - A flag indicating whether the callable is implicit (e.g., a default constructor). + * @property {boolean} is_constructor - A flag indicating whether the callable is a constructor. + * @property {JComment[]} comments - A list of comments associated with the callable. + * @property {string[]} annotations - The annotations applied to the callable. + * @property {string[]} modifiers - The modifiers applied to the callable (e.g., public, static). + * @property {string[]} thrown_exceptions - Exceptions declared via "throws". + * @property {string} declaration - The declaration of the callable. + * @property {JCallableParameter[]} parameters - The parameters of the callable. + * @property {string|null} return_type - The return type of the callable. Null if the callable does not return a value. + * @property {string} code - The code block of the callable. + * @property {number} start_line - The starting line number of the callable in the source file. + * @property {number} end_line - The ending line number of the callable in the source file. + * @property {string[]} referenced_types - The types referenced within the callable. + * @property {string[]} accessed_fields - Fields accessed in the callable. + * @property {JCallSite[]} call_sites - Call sites in the callable. + * @property {boolean} is_entrypoint - A flag indicating whether this is a service entry point method. + * @property {JVariableDeclaration[]} variable_declarations - Local variable declarations in the callable. + * @property {JCRUDOperation[]|null} crud_operations - CRUD operations in the callable. + * @property {JCRUDQuery[]|null} crud_queries - CRUD queries in the callable. + * @property {number|null} cyclomatic_complexity - Cyclomatic complexity of the callable. + */ +export const JCallable = z.object({ + signature: z.string(), + is_implicit: z.boolean(), + is_constructor: z.boolean(), + comments: z.array(JComment), + annotations: z.array(z.string()), + modifiers: z.array(z.string()), + thrown_exceptions: z.array(z.string()).default([]), + declaration: z.string(), + parameters: z.array(JCallableParameter), + return_type: z.string().nullable(), + code: z.string(), + start_line: z.number(), + end_line: z.number(), + referenced_types: z.array(z.string()), + accessed_fields: z.array(z.string()), + call_sites: z.array(JCallSite), + is_entrypoint: z.boolean().default(false), + variable_declarations: z.array(JVariableDeclaration), + crud_operations: z.array(JCRUDOperation).nullable(), + crud_queries: z.array(JCRUDQuery).nullable(), + cyclomatic_complexity: z.number().nullable() +}); + +/** + * Represents a Java class or interface. + * + * @typedef {Object} JType + * @property {boolean} is_interface - A flag indicating whether the object is an interface. + * @property {boolean} is_inner_class - A flag indicating whether the object is an inner class. + * @property {boolean} is_local_class - A flag indicating whether the object is a local class. + * @property {boolean} is_nested_type - A flag indicating whether the object is a nested type. + * @property {boolean} is_class_or_interface_declaration - A flag indicating whether the object is a class or interface declaration. + * @property {boolean} is_enum_declaration - A flag indicating whether the object is an enum declaration. + * @property {boolean} is_annotation_declaration - A flag indicating whether the object is an annotation declaration. + * @property {boolean} is_record_declaration - A flag indicating whether this object is a record declaration. + * @property {boolean} is_concrete_class - A flag indicating whether this is a concrete class. + * @property {JComment[]|null} comments - A list of comments associated with the class/type. + * @property {string[]|null} extends_list - The list of classes or interfaces that the object extends. + * @property {string[]|null} implements_list - The list of interfaces that the object implements. + * @property {string[]|null} modifiers - The list of modifiers of the object. + * @property {string[]|null} annotations - The list of annotations of the object. + * @property {string} parent_type - The name of the parent class (if it exists). + * @property {string[]|null} nested_type_declarations - All the class declarations nested under this class. + * @property {Record} callable_declarations - The list of constructors and methods of the object. + * @property {JField[]} field_declarations - The list of fields of the object. + * @property {JEnumConstant[]|null} enum_constants - The list of enum constants in the object. + * @property {JRecordComponent[]|null} record_components - The list of record components in the object. + * @property {InitializationBlock[]|null} initialization_blocks - The list of initialization blocks in the object. + * @property {boolean} is_entrypoint_class - A flag indicating whether this is a service entry point class. + */ +export const JType = z.object({ + is_interface: z.boolean().default(false), + is_inner_class: z.boolean().default(false), + is_local_class: z.boolean().default(false), + is_nested_type: z.boolean().default(false), + is_class_or_interface_declaration: z.boolean().default(false), + is_enum_declaration: z.boolean().default(false), + is_annotation_declaration: z.boolean().default(false), + is_record_declaration: z.boolean().default(false), + is_concrete_class: z.boolean().default(false), + comments: z.array(JComment).nullable().default([]), + extends_list: z.array(z.string()).nullable().default([]), + implements_list: z.array(z.string()).nullable().default([]), + modifiers: z.array(z.string()).nullable().default([]), + annotations: z.array(z.string()).nullable().default([]), + parent_type: z.string(), + nested_type_declarations: z.array(z.string()).nullable().default([]), + callable_declarations: z.record(z.string(), JCallable).default({}), + field_declarations: z.array(JField).default([]), + enum_constants: z.array(JEnumConstant).nullable().default([]), + record_components: z.array(JRecordComponent).nullable().default([]), + initialization_blocks: z.array(InitializationBlock).nullable().default([]), + is_entrypoint_class: z.boolean().default(false) +}); + +/** + * Represents a compilation unit in Java. + * + * @typedef {Object} JCompilationUnit + * @property {JComment[]} comments - A list of comments in the compilation unit. + * @property {string[]} imports - A list of import statements in the compilation unit. + * @property {Record} type_declarations - A dictionary mapping type names to their corresponding JType representations. + * @property {boolean} is_modified - A flag indicating whether the compilation unit has been modified. + */ +export const JCompilationUnit = z.object({ + comments: z.array(JComment), + imports: z.array(z.string()), + type_declarations: z.record(z.string(), JType), + is_modified: z.boolean().default(false) +}); + +/** + * Represents details about a method in a Java class. + * + * @typedef {Object} JMethodDetail + * @property {string} method_declaration - The declaration string of the method. + * @property {string} klass - The name of the class containing the method. + * @property {JCallable} method - An instance of JCallable representing the callable details of the method. + */ +export const JMethodDetail = z.object({ + method_declaration: z.string(), + klass: z.string(), + method: JCallable +}); + +const _metaMethodDetail = z.object({ + file_path: z.string(), + type_declaration: z.string(), + signature: z.string(), + callable_declaration: z.string() +}); + +export const JGraphEdges = z.object({ + source: _metaMethodDetail.transform(buildMethodDetail), + target: _metaMethodDetail.transform(buildMethodDetail), + type: z.string(), + weight: z.number(), + source_kind: z.string().nullable().optional(), + target_kind: z.string().nullable().optional(), +}); + +const JSymbolTable = z.record(z.string(), JCompilationUnit).transform(symbolTable => { + for (const [_, compilationUnit] of Object.entries(symbolTable)) { + // Check if type_declarations exists before trying to iterate over it + if (compilationUnit && 'type_declarations' in compilationUnit) { + for (const [typeName, jType] of Object.entries(compilationUnit.type_declarations)) { + // Check if callable_declarations exists before trying to iterate over it + if (jType && jType.callable_declarations) { + for (const [_, callable] of Object.entries(jType.callable_declarations)) { + addToLookupTable(typeName, callable.signature, callable); + } + } + } + } + } + return symbolTable; +}); + +export const JApplication = z.object({ + symbol_table: JSymbolTable, + call_graph: z.array(JGraphEdges).nullable().optional(), + system_dependency_graph: z.array(JGraphEdges).nullable().optional() +}); \ No newline at end of file diff --git a/src/models/java/types.ts b/src/models/java/types.ts new file mode 100644 index 0000000..76549da --- /dev/null +++ b/src/models/java/types.ts @@ -0,0 +1,40 @@ +/** + * Copyright IBM Corporation 2025 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { z } from 'zod'; +import * as schema from './schema'; +import { CRUDOperationType, CRUDQueryType } from './enums'; + +// Export all types inferred from Zod schemas +export type JCommentType = z.infer; +export type JRecordComponentType = z.infer; +export type JFieldType = z.infer; +export type JCallableParameterType = z.infer; +export type JEnumConstantType = z.infer; +export type JCRUDOperationType = z.infer; +export type JCRUDQueryType = z.infer; +export type JCallSiteType = z.infer; +export type JVariableDeclarationType = z.infer; +export type InitializationBlockType = z.infer; +export type JCallableType = z.infer; +export type JTypeType = z.infer; +export type JCompilationUnitType = z.infer; +export type JMethodDetailType = z.infer; +export type JGraphEdgesType = z.infer; +export type JApplicationType = z.infer; + +// Re-export the enums for convenience +export { CRUDOperationType, CRUDQueryType }; \ No newline at end of file diff --git a/test/conftest.ts b/test/conftest.ts new file mode 100644 index 0000000..7ce2d0b --- /dev/null +++ b/test/conftest.ts @@ -0,0 +1,69 @@ +/** + * Copyright IBM Corporation 2025 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Setup code for the test session. + */ + +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 { CLDK } from "../src/CLDK"; + +/* + * Set up sample applications for testing + * This is a one-time setup. Daytrader 8 will be downloaded and extracted + */ +let dayTraderApp: string; +export let daytraderJavaAnalysis: JavaAnalysis; + +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}); + + /** + * I am just hardcoding the extracted directory name for now. The extracted directory name would follow GitHub's + * repository zip extraction convention. The extracted directory would be named in the format {repo-name}-{tag}. + * For the URL: https://github.com/OpenLiberty/sample.daytrader8/archive/refs/tags/v1.2.zip, the repository name + * is sample.daytrader8 and the tag is v1.2. So the extracted directory would be sample.daytrader8-1.2 (note that + * GitHub typically removes the "v" prefix from version tags in the extracted directory name). + */ + // Set the dayTraderApp variable to the extracted directory + dayTraderApp = extractedDir; + + /** + * Let's also create the JavaAnalysis instance here for testing purposes. + */ + daytraderJavaAnalysis = CLDK.for("java").analysis({ + projectPath: extractedDir, + analysisLevel: "symbol table", + }) +}) + +/** + * Tear down the test environment + * Remove the daytrader application directory (but keep the zip file) + */ +afterAll(async () => { + if (dayTraderApp) { + fs.rmSync(dayTraderApp, {recursive: true, force: true}); + } +}) + diff --git a/test/test-applications/java/sample.daytrader8-1.2.zip b/test/test-applications/java/sample.daytrader8-1.2.zip new file mode 100644 index 0000000..526e9df Binary files /dev/null and b/test/test-applications/java/sample.daytrader8-1.2.zip differ diff --git a/test/unit/CLDK.test.ts b/test/unit/CLDK.test.ts new file mode 100644 index 0000000..0dccb82 --- /dev/null +++ b/test/unit/CLDK.test.ts @@ -0,0 +1,56 @@ +import { CLDK } from "../../src"; +import { test, expect } from "bun:test"; + +/** + * These first set of tests are to test the CLDK class + */ +test("CLDK initialization with Java language", () => { + expect(CLDK.for("java").getLanguage()).toBe("java"); +}); + +test("CLDK must throw and error when the language is not Java", () => { + expect(() => CLDK.for("python").analysis({ + projectPath: "fake/path", + analysisLevel: "Symbol Table", + })).toThrowError("Analysis support for python is not implemented yet."); +}); + + +test("CLDK Analysis level must be set to 1 for symbol table", () => { + const analysis = CLDK.for("java").analysis({ + projectPath: "fake/path", + analysisLevel: "Symbol Table", + }); + expect(analysis.analysisLevel).toBe("1"); +}); + +test("CLDK Analysis level must be set to 2 for call graph", () => { + const analysis = CLDK.for("java").analysis({ + projectPath: "fake/path", + analysisLevel: "Call Graph", + }); + expect(analysis.analysisLevel).toBe("2"); +}); + +test("CLDK Analysis level must be set to 3 for system dependency graph", () => { + const analysis = CLDK.for("java").analysis({ + projectPath: "fake/path", + analysisLevel: "system dependency graph", + }); + expect(analysis.analysisLevel).toBe("3"); +}); + +/** + * Okay, so now we can test if we can call codeanalyzer with the right arguments + */ +test("CLDK must get the correct codeanalyzer execution command", () => { + const analysis = CLDK.for("java").analysis({ + projectPath: "fake/path", + analysisLevel: "Symbol Table", + }); + + const codeAnalyzerExec = analysis.getCodeAnalyzerExec(); + expect(codeAnalyzerExec[0]).toBe("java"); + expect(codeAnalyzerExec[1]).toBe("-jar"); + expect(codeAnalyzerExec[2]).toMatch(/codeanalyzer-.*\.jar/); +}); \ No newline at end of file diff --git a/test/unit/analysis/java/JavaAnalysis.test.ts b/test/unit/analysis/java/JavaAnalysis.test.ts new file mode 100644 index 0000000..b8c1844 --- /dev/null +++ b/test/unit/analysis/java/JavaAnalysis.test.ts @@ -0,0 +1,16 @@ +import {daytraderJavaAnalysis} from "../../../conftest"; +import {expect, test } from "bun:test"; + +test("Must get analysis object from JavaAnalysis object", () => { + expect(daytraderJavaAnalysis).toBeDefined(); +}); + +test("Must 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(); +}); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..0b14a20 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,33 @@ +{ + "compilerOptions": { + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "outDir": "dist", + // Environment setup & latest features + "lib": [ + "ESNext" + ], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + }, + "include": [ + "src" + ] +}