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

codellm-devkit / typescript-sdk / 14688373625

27 Apr 2025 04:24AM UTC coverage: 87.327%. First build
14688373625

Pull #2

github

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

140 of 141 new or added lines in 2 files covered. (99.29%)

379 of 434 relevant lines covered (87.33%)

41.21 hits per line

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

99.13
/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";
54✔
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

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
      console.log("Default codeanalyzer jar not found.");
57✔
60
      throw new Error("Default codeanalyzer jar not found.");
59✔
61
    }
9✔
62
    log.info("Codeanalyzer jar found at:", jarPath);
104✔
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
      log.debug(command.join(" "));
70✔
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 stream = fs.createReadStream(path.join(tmpFilePath, "analysis.json")).pipe(JSONStream.parse());
218✔
110
        const result = {} as types.JApplicationType;
52✔
111

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

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

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

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

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

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

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

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

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

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

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