• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

codellm-devkit / typescript-sdk / 14708616944

28 Apr 2025 01:10PM UTC coverage: 83.75%. First build
14708616944

Pull #2

github

web-flow
Merge cc797b646 into 2ebd6e0b6
Pull Request #2: Lightweight APIs to analyze Java projects just with it's Symbol Table

167 of 191 new or added lines in 5 files covered. (87.43%)

402 of 480 relevant lines covered (83.75%)

41.01 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.51
/src/analysis/java/JavaAnalysis.ts
1
/**
2
 * Copyright IBM Corporation 2025
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *       http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16

17
import path from "path";
58✔
18
import fg from "fast-glob";
54✔
19
import fs from "fs";
50✔
20
import log from "loglevel";
21
import { spawnSync } from "node:child_process";
94✔
22
import { JApplication, JCompilationUnit } from "../../models/java";
98✔
23
import * as types from "../../models/java/types";
24
import { JType } from "../../models/java";
25
import os from "os";
50✔
26
import JSONStream from "JSONStream";
72✔
27
declare module "JSONStream";
28
import crypto from "crypto";
66✔
29
import { createLogger } from "src/utils";
82✔
30

31
const logger = createLogger("JavaAnalysis");
88✔
32

33
enum AnalysisLevel {
126✔
34
  SYMBOL_TABLE = "1",
76✔
35
  CALL_GRAPH = "2",
72✔
36
  SYSTEM_DEPENDENCY_GRAPH = "3",
100✔
37
}
38

39
const analysisLevelMap: Record<string, AnalysisLevel> = {
54✔
40
  "symbol table": AnalysisLevel.SYMBOL_TABLE,
82✔
41
  "call graph": AnalysisLevel.CALL_GRAPH,
74✔
42
  "system dependency graph": AnalysisLevel.SYSTEM_DEPENDENCY_GRAPH,
122✔
43
};
3✔
44

45
export class JavaAnalysis {
46
  private readonly projectDir: string | null;
47
  private analysisLevel: AnalysisLevel;
48
  application?: types.JApplicationType;
1✔
49

50
  constructor(options: { projectDir: string | null; analysisLevel: string }) {
41✔
51
    this.projectDir = options.projectDir;
82✔
52
    this.analysisLevel = analysisLevelMap[options.analysisLevel.toLowerCase()] ?? AnalysisLevel.SYMBOL_TABLE;
210✔
53
  }
54

55
  private getCodeAnalyzerExec(): string[] {
35✔
56
    const codeanalyzerJarPath = path.resolve(__dirname, "jars");
128✔
57
    const pattern = path.join(codeanalyzerJarPath, "**/codeanalyzer-*.jar").replace(/\\/g, "/");
192✔
58
    const matches = fg.sync(pattern);
74✔
59
    const jarPath = matches[0];
62✔
60

61
    if (!jarPath) {
34✔
62
      logger.error("Default codeanalyzer jar not found.");
58✔
63
      throw new Error("Default codeanalyzer jar not found.");
59✔
64
    }
9✔
65
    logger.info("Codeanalyzer jar found at:", jarPath);
110✔
66
    return ["java", "-jar", jarPath];
76✔
67
  }
68

69
  /**
70
   * Initialize the application by running the codeanalyzer and parsing the output.
71
   * @private
72
   * @returns {Promise<types.JApplicationType>} A promise that resolves to the parsed application data
73
   * @throws {Error} If the project directory is not specified or if codeanalyzer fails
74
   */
75
  private async _initialize_application(): Promise<types.JApplicationType> {
39✔
76
    return new Promise<types.JApplicationType>((resolve, reject) => {
93✔
77
      if (!this.projectDir) {
52✔
78
        return reject(new Error("Project directory not specified"));
66✔
79
      }
13✔
80

81
      const projectPath = path.resolve(this.projectDir);
112✔
82
      // Create a temporary file to store the codeanalyzer output
83
      const tmpFilePath = path.join(os.tmpdir(), `${Date.now()}-${crypto.randomUUID()}`);
178✔
84
      const command = [
50✔
85
        ...this.getCodeAnalyzerExec(),
76✔
86
        "--input",
36✔
87
        projectPath,
40✔
88
        "--output",
38✔
89
        tmpFilePath,
40✔
90
        `--analysis-level=${this.analysisLevel}`,
98✔
91
        "--verbose",
34✔
92
      ];
16✔
93
      // Check if command is valid
94
      if (!command[0]) {
42✔
95
        return reject(new Error("Codeanalyzer command not found"));
65✔
96
      }
13✔
97
      logger.debug(command.join(" "));
76✔
98
      const result = spawnSync(command[0], command.slice(1), {
128✔
99
        stdio: ["ignore", "pipe", "inherit"],
84✔
100
      });
18✔
101

102
      if (result.error) {
44✔
103
        return reject(result.error);
34✔
104
      }
13✔
105

106
      if (result.status !== 0) {
58✔
107
        return reject(new Error("Codeanalyzer failed to run."));
62✔
108
      }
13✔
109

110
      // Read the analysis result from the temporary file
111
      try {
26✔
112
        const stream = fs.createReadStream(path.join(tmpFilePath, "analysis.json")).pipe(JSONStream.parse());
218✔
113
        const result = {} as types.JApplicationType;
52✔
114

115
        stream.on("data", (data: unknown) => {
77✔
116
          Object.assign(result, JApplication.parse(data));
113✔
117
        });
20✔
118

119
        stream.on("end", () => {
67✔
120
          // Clean up the temporary file
121
          logger.debug(`Deleting temporary file: ${tmpFilePath}`);
132✔
122
          fs.rm(tmpFilePath, { recursive: true, force: true }, (err) => {
149✔
123
            if (err) logger.warn(`Failed to delete temporary file: ${tmpFilePath}`, err);
132✔
124
          });
24✔
125
          resolve(result as types.JApplicationType);
49✔
126
        });
20✔
127

NEW
128
        stream.on("error", (err: any) => {
×
129
          reject(err);
21✔
130
        });
18✔
131
      } catch (error) {
23✔
132
        reject(error);
30✔
133
      }
134
    });
15✔
135
  }
136

137
  /**
138
   * Get the application data. This method returns the parsed Java application as a JSON structure containing the
139
   * following information:
140
   * |_ symbol_table: A record of file paths to compilation units. Each compilation unit further contains:
141
   *   |_ comments: Top-level file comments
142
   *   |_ imports: All import statements
143
   *   |_ type_declarations: All class/interface/enum/record declarations with their:
144
   *     |_ fields, methods, constructors, initialization blocks, etc.
145
   * |_ call_graph: Method-to-method call relationships (if analysis level ≥ 2)
146
   * |_ system_dependency_graph: System component dependencies (if analysis level = 3)
147
   *
148
   * The application view denoted by this application structure is crucial for further fine-grained analysis APIs.
149
   * If the application is not already initialized, it will be initialized first.
150
   * @returns {Promise<types.JApplicationType>} A promise that resolves to the application data
151
   */
152
  public async getApplication(): Promise<types.JApplicationType> {
30✔
153
    if (!this.application) {
59✔
154
      this.application = await this._initialize_application();
120✔
155
    }
9✔
156
    return this.application;
59✔
157
  }
158

159
  /**
160
   * Get the symbol table from the application.
161
   * @returns {Promise<Record<string, types.JCompilationUnitType>>} A promise that resolves to a record of file paths and their
162
   * corresponding {@link JCompilationUnitType} objects
163
   * 
164
   * @notes This method retrieves the symbol table from the application, which contains information about all
165
   * compilation units in the Java application. The returned record contains file paths as keys and their
166
   * corresponding {@link JCompilationUnit} objects as values.
167
   */
168
  public async getSymbolTable(): Promise<Record<string, types.JCompilationUnitType>> {
30✔
169
    return (await this.getApplication()).symbol_table;
111✔
170
  }
171

172
  /**
173
   * Get all classes in the application.
174
   * @returns {Promise<Record<string, types.JTypeType>>} A promise that resolves to a record of class names and their
175
   * corresponding {@link JTypeType} objects
176
   *
177
   * @notes This method retrieves all classes from the symbol table and returns them as a record. The returned record
178
   *       contains class names as keys and their corresponding {@link JType} objects as values.
179
   */
180
  public async getAllClasses(): Promise<Record<string, types.JTypeType>> {
29✔
181
    return Object.values(await this.getSymbolTable()).reduce((classAccumulator, symbol) => {
187✔
182
      Object.entries(symbol.type_declarations).forEach(([key, value]) => {
151✔
183
        classAccumulator[key] = value;
73✔
184
      });
16✔
185
      return classAccumulator;
56✔
186
    }, {} as Record<string, types.JTypeType>);
19✔
187
  }
188

189
  /**
190
   * Get a specific class by its qualified name.
191
   * @param {string} qualifiedName - The qualified name of the class to retrieve
192
   * @returns {Promise<types.JTypeType>} A promise that resolves to the {@link JTypeType} object representing the class
193
   * @throws {Error} If the class is not found in the application
194
   * 
195
   * @notes This method retrieves a specific class from the application by its qualified name. If the class is found,
196
   *       it returns the corresponding {@link JType} object. If the class is not found, it throws an error.
197
   */
198
  public async getClassByQualifiedName(qualifiedName: string): Promise<types.JTypeType> {
65✔
199
    const allClasses = await this.getAllClasses();
100✔
200
    if (allClasses[qualifiedName]) {
75✔
201
      return allClasses[qualifiedName];
69✔
202
    }
3✔
203
    else
204
      throw new Error(`Class ${qualifiedName} not found in the application.`);
175✔
205
  }
206

207
  /**
208
  * Get all methods in the application.
209
  * @returns {Promise<Record<string, Record<string, types.JCallableType>>>} A promise that resolves to a record of
210
  * method names and their corresponding {@link JCallableType} objects
211
  * 
212
  * @notes This method retrieves all methods from the symbol table and returns them as a record. The returned
213
  *       record contains class names as keys and their corresponding {@link JCallableType} objects as values.
214
  *       Each {@link JCallableType} object contains information about the method's parameters, return type, and
215
  *       other relevant details.
216
  */
217
  public async getAllMethods(): Promise<Record<string, Record<string, types.JCallableType>>> {
29✔
218
    return Object.entries(await this.getAllClasses()).reduce((allMethods, [key, value]) => {
187✔
219
      allMethods[key] = value.callable_declarations;
104✔
220
      return allMethods;
44✔
221
    }, {} as Record<string, Record<string, types.JCallableType>>);
19✔
222
  }
223

224
  /**
225
   * Get all methods in a specific class in the application.
226
   * @returns {Promise<Record<string, Record<string, types.JCallableType>>>} A promise that resolves to a record of
227
   * method names and their corresponding {@link JCallableType} objects
228
   * 
229
   * @notes This method retrieves all methods from the symbol table and returns them as a record. The returned
230
   *       record contains class names as keys and their corresponding {@link JCallableType} objects as values.
231
   *       Each {@link JCallableType} object contains information about the method's parameters, return type, and
232
   *       other relevant details.
233
   */
234
  public async getAllMethodsByClass(qualifiedName: string): Promise<Array<types.JCallableType>> {
62✔
235
    const classForWhichMethodsAreRequested = await this.getClassByQualifiedName(qualifiedName);
190✔
236
    return classForWhichMethodsAreRequested ? Object.values(classForWhichMethodsAreRequested.callable_declarations ?? {}) : [];
251✔
237
  }
238

239
  /**
240
   * Get a specific methods within a specific class by its qualified name.
241
   * @param {string} qualifiedName - The qualified name of the class to retrieve
242
   * @param {string} methodName - The name of the method to retrieve
243
   * @returns {Promise<types.JCallableType>} A promise that resolves to the {@link JCallable} object representing the method.
244
   * @throws {Error} If the class or method is not found in the application.
245
   * 
246
   * @notes This method retrieves a specific method from the application by its qualified name and method name.
247
   * If the method is found, it returns the corresponding {@link JCallableType} object. If the method is not found,
248
   * it throws an error.
249
   */
250
  public async getMethodByQualifiedName(qualifiedName: string, methodName: string): Promise<types.JCallableType> {
90✔
251
    return (await this.getAllMethodsByClass(qualifiedName)).find(
121✔
252
      (method) => method.signature === methodName
85✔
253
    ) ?? (() => { throw new Error(`Method ${methodName} not found in class ${qualifiedName}.`); })();
125✔
254
  }
255

256
  /**
257
   * Get all the method parameters in a specific method within a specific class by its qualified name.
258
   * @param {string} qualifiedName - The qualified name of the class to retrieve
259
   * @param {string} methodName - The name of the method to retrieve
260
   * @returns {Promise<Array<types.JCallableParameterType>>} A promise that resolves to an array of {@link JCallableParameterType} objects
261
   * @throws {Error} If the class or method is not found in the application.
262
   * 
263
   * @notes This method retrieves all the parameters of a specific method from the application by its qualified name
264
   * and method name. If the method is found, it returns an array of {@link JCallableParameter} objects representing
265
   */
266
  public async getMethodParameters(qualifiedName: string, methodName: string): Promise<Array<types.JCallableParameterType>> {
85✔
267
    return (await this.getMethodByQualifiedName(qualifiedName, methodName)).parameters ?? [];
189✔
268
  }
269

270
  /**
271
   * Get all the method parameters in a specific method within a specific class by its callable object.
272
   * @param {types.JCallableType} callable - The callable object representing the method to retrieve
273
   * @returns {Promise<Array<types.JCallableParameterType>>} A promise that resolves to an array of {@link JCallableParameterType} objects
274
   * 
275
   * @notes This method retrieves all the parameters of a specific method from the application by its callable object.
276
   * If the method is found, it returns an array of {@link JCallableParameter} objects representing
277
   * the parameters of the method. Otherwise, it returns an empty array.
278
   */
279
  public async getMethodParametersFromCallable(callable: types.JCallableType): Promise<Array<types.JCallableParameterType>> {
63✔
NEW
280
    return callable.parameters ?? [];
×
NEW
281
  }
×
NEW
282

×
NEW
283
  /**
×
NEW
284
   * Get the java file path given the qualified name of the class.
×
NEW
285
   * @param {string} qualifiedName - The qualified name of the class to retrieve
×
NEW
286
   * @returns {Promise<string>} A promise that resolves to the file path of the Java file containing the class
×
NEW
287
   * @throws {Error} If the class is not found in the application.
×
NEW
288
   * 
×
NEW
289
   * @notes This method retrieves the file path of the Java file containing the class with the specified qualified name.
×
NEW
290
   * If the class is found, it returns the file path as a string. If the class is not found, it throws an error.
×
NEW
291
   */
×
NEW
292
  public async getJavaFilePathByQualifiedName(qualifiedName: string): Promise<string> {
×
NEW
293
    const symbolTable = await this.getSymbolTable();
×
NEW
294
    for (const [filePath, compilationUnit] of Object.entries(symbolTable)) {
×
NEW
295
      if (Object.keys(compilationUnit.type_declarations).includes(qualifiedName)) {
×
NEW
296
        return filePath;
×
NEW
297
      }
×
NEW
298
    }
×
299
    throw new Error(`Class ${qualifiedName} not found in the application.`);
77✔
300
  }
301
}
66✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc