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

codellm-devkit / typescript-sdk / 14843627684

05 May 2025 06:36PM CUT coverage: 88.273%. First build
14843627684

Pull #2

github

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

179 of 180 new or added lines in 5 files covered. (99.44%)

414 of 469 relevant lines covered (88.27%)

44.36 hits per line

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

99.21
/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 { spawnSync } from "node:child_process";
94✔
21
import { JApplication, JCompilationUnit } from "../../models/java";
98✔
22
import * as types from "../../models/java/types";
23
import { JType } from "../../models/java";
24
import os from "os";
50✔
25
import crypto from "crypto";
66✔
26
import { createLogger } from "src/utils";
82✔
27

28
const logger = createLogger("JavaAnalysis");
88✔
29

30
enum AnalysisLevel {
126✔
31
  SYMBOL_TABLE = "1",
76✔
32
  CALL_GRAPH = "2",
72✔
33
  SYSTEM_DEPENDENCY_GRAPH = "3",
100✔
34
}
35

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

42
export class JavaAnalysis {
43
  private readonly projectDir: string | null;
44
  private analysisLevel: AnalysisLevel;
45
  application?: types.JApplicationType;
1✔
46

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

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

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

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

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

99
      if (result.error) {
44✔
100
        return reject(result.error);
34✔
101
      }
13✔
102

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

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

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

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

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

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

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

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

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

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

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

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

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

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

281
  /**
282
   * Get the java file path given the qualified name of the class.
283
   * @param {string} qualifiedName - The qualified name of the class to retrieve
284
   * @returns {Promise<string>} A promise that resolves to the file path of the Java file containing the class
285
   * @throws {Error} If the class is not found in the application.
286
   * 
287
   * @notes This method retrieves the file path of the Java file containing the class with the specified qualified name.
288
   * If the class is found, it returns the file path as a string. If the class is not found, it throws an error.
289
   */
290
  public async getJavaFilePathByQualifiedName(qualifiedName: string): Promise<string> {
72✔
291
    const symbolTable = await this.getSymbolTable();
104✔
292
    for (const [filePath, compilationUnit] of Object.entries(symbolTable)) {
155✔
293
      if (Object.keys(compilationUnit.type_declarations).includes(qualifiedName)) {
169✔
294
        return filePath;
37✔
295
      }
9✔
296
    }
5✔
297
    throw new Error(`Class ${qualifiedName} not found in the application.`);
78✔
298
  }
299

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

© 2025 Coveralls, Inc