• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (c) 2024-2025 Huawei Device Co., Ltd.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 *     http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15
16import path from 'path';
17import fs from 'fs';
18import { Command, InvalidArgumentError } from 'commander';
19import { PrinterBuilder } from './PrinterBuilder';
20import { SceneConfig } from '../Config';
21import { Scene } from '../Scene';
22import { ArkFile } from '../core/model/ArkFile';
23import { JsonPrinter } from './JsonPrinter';
24import { Printer } from './Printer';
25import { PointerAnalysis } from '../callgraph/pointerAnalysis/PointerAnalysis';
26
27export function buildSceneFromSingleFile(filename: string, verbose: boolean = false): Scene {
28    if (verbose) {
29        console.log('Building scene...');
30    }
31    const filepath = path.resolve(filename);
32    const projectDir = path.dirname(filepath);
33    const config = new SceneConfig();
34    config.buildConfig('single-file', projectDir, []);
35    config.getProjectFiles().push(filepath);
36    const scene = new Scene();
37    scene.buildSceneFromProjectDir(config);
38    return scene;
39}
40
41export function buildSceneFromProjectDir(inputDir: string, verbose: boolean = false): Scene {
42    if (verbose) {
43        console.log('Building scene...');
44    }
45    const config = new SceneConfig();
46    config.buildFromProjectDir(inputDir);
47    const scene = new Scene();
48    scene.buildSceneFromProjectDir(config);
49    return scene;
50}
51
52export function serializeArkFile(arkFile: ArkFile, output?: string): void {
53    let filename = output;
54    if (filename === undefined) {
55        const outputDir = path.join(arkFile.getProjectDir(), '..', 'output');
56        filename = path.join(outputDir, arkFile.getName() + '.json');
57    }
58    fs.mkdirSync(path.dirname(filename), { recursive: true });
59    let printer: Printer = new JsonPrinter(arkFile);
60    const fd = fs.openSync(filename, 'w');
61    fs.writeFileSync(fd, printer.dump());
62    fs.closeSync(fd);
63}
64
65export function serializeScene(scene: Scene, outDir: string, verbose: boolean = false): void {
66    let files = scene.getFiles();
67    console.log(`Serializing Scene with ${files.length} files to '${outDir}'...`);
68    for (let f of files) {
69        let filepath = f.getName();
70        let outPath = path.join(outDir, filepath + '.json');
71        if (verbose) {
72            console.log(`Serializing ArkIR for '${filepath}' to '${outPath}'...`);
73        }
74        serializeArkFile(f, outPath);
75    }
76    if (verbose) {
77        console.log(`All ${files.length} files in scene are serialized`);
78    }
79}
80
81function serializeSingleTsFile(input: string, output: string, options: any): void {
82    options.verbose && console.log(`Serializing TS file to JSON: '${input}' -> '${output}'`);
83
84    let filepath = path.resolve(input);
85    let projectDir = path.dirname(filepath);
86
87    const scene = buildSceneFromSingleFile(filepath, options.verbose);
88
89    let files = scene.getFiles();
90    if (options.verbose) {
91        console.log(`Scene contains ${files.length} files`);
92        for (let f of files) {
93            console.log(`- '${f.getName()}'`);
94        }
95    }
96
97    if (options.inferTypes) {
98        options.verbose && console.log('Inferring types...');
99        scene.inferTypes();
100        if (options.inferTypes > 1) {
101            for (let i = 1; i < options.inferTypes; i++) {
102                options.verbose && console.log(`Inferring types one more time (${i + 1} / ${options.inferTypes})...`);
103                scene.inferTypes();
104            }
105        }
106    }
107
108    if (options.entrypoint) {
109        options.verbose && console.log('Generating entrypoint...');
110        PointerAnalysis.pointerAnalysisForWholeProject(scene);
111    }
112
113    options.verbose && console.log('Extracting single ArkFile...');
114
115    if (files.length === 0) {
116        console.error(`ERROR: No files found in the project directory '${projectDir}'.`);
117        process.exit(1);
118    }
119    if (files.length > 1) {
120        console.error(`ERROR: More than one file found in the project directory '${projectDir}'.`);
121        process.exit(1);
122    }
123    // Note: we explicitly push a single path to the project files (in config),
124    //       so we expect there is only *one* ArkFile in the scene.
125    let arkFile = scene.getFiles()[0];
126    serializeFile(arkFile, output, options, scene);
127
128    options.verbose && console.log('All done!');
129}
130
131function serializeFile(arkFile: ArkFile, output: string, options: any, scene: Scene): void {
132    let outPath: string;
133    if (fs.existsSync(output) && fs.statSync(output).isDirectory()) {
134        outPath = path.join(output, arkFile.getName() + '.json');
135    } else if (!fs.existsSync(output) && output.endsWith('/')) {
136        outPath = path.join(output, arkFile.getName() + '.json');
137    } else {
138        outPath = output;
139    }
140
141    console.log(`Serializing ArkIR for '${arkFile.getName()}' to '${outPath}'...`);
142    let printer = new PrinterBuilder();
143    printer.dumpToJson(arkFile, outPath);
144
145    if (options.entrypoint) {
146        let arkFile = scene.getFiles()[1];
147        let outPath: string;
148        if (fs.existsSync(output) && fs.statSync(output).isDirectory()) {
149            outPath = path.join(output, arkFile.getName() + '.json');
150        } else if (!fs.existsSync(output) && output.endsWith('/')) {
151            outPath = path.join(output, arkFile.getName() + '.json');
152        } else {
153            outPath = path.join(path.dirname(output), arkFile.getName() + '.json');
154        }
155        console.log(`Serializing entrypoint to '${outPath}'...`);
156        printer.dumpToJson(arkFile, outPath);
157    }
158}
159
160function serializeMultipleTsFiles(inputDir: string, outDir: string, options: any): void {
161    console.log(`Serializing multiple TS files to JSON: '${inputDir}' -> '${outDir}'`);
162    if (fs.existsSync(outDir) && !fs.statSync(outDir).isDirectory()) {
163        console.error(`ERROR: Output path must be a directory.`);
164        process.exit(1);
165    }
166
167    if (options.verbose) {
168        console.log('Building scene...');
169    }
170    let config = new SceneConfig();
171    config.buildFromProjectDir(inputDir);
172    let scene = new Scene();
173    scene.buildSceneFromProjectDir(config);
174
175    let files = scene.getFiles();
176    if (options.verbose) {
177        console.log(`Scene contains ${files.length} files`);
178        files.forEach(f => console.log(`- '${f.getName()}'`));
179    }
180
181    if (options.inferTypes) {
182        if (options.verbose) {
183            console.log('Inferring types...');
184        }
185        scene.inferTypes();
186        if (options.inferTypes > 1) {
187            for (let i = 1; i < options.inferTypes; i++) {
188                options.verbose && console.log(`Inferring types one more time (${i + 1} / ${options.inferTypes})...`);
189                scene.inferTypes();
190            }
191        }
192    }
193
194    if (options.entrypoint) {
195        if (options.verbose) {
196            console.log('Generating entrypoint...');
197        }
198        PointerAnalysis.pointerAnalysisForWholeProject(scene);
199        files = scene.getFiles();
200    }
201
202    if (options.verbose) {
203        console.log('Serializing...');
204    }
205    let printer = new PrinterBuilder();
206    for (let f of files) {
207        let filepath = f.getName();
208        let outPath = path.join(outDir, filepath + '.json');
209        console.log(`Serializing ArkIR for '${filepath}' to '${outPath}'...`);
210        printer.dumpToJson(f, outPath);
211    }
212    console.log('All done!');
213}
214
215function serializeTsProject(inputDir: string, outDir: string, options: any): void {
216    console.log(`Serializing TS project to JSON: '${inputDir}' -> '${outDir}'`);
217
218    if (fs.existsSync(outDir) && !fs.statSync(outDir).isDirectory()) {
219        console.error(`ERROR: Output path must be a directory.`);
220        process.exit(1);
221    }
222
223    const scene = buildSceneFromProjectDir(inputDir, options.verbose);
224
225    if (options.inferTypes) {
226        if (options.verbose) {
227            console.log('Inferring types...');
228        }
229        scene.inferTypes();
230        if (options.inferTypes > 1) {
231            for (let i = 1; i < options.inferTypes; i++) {
232                options.verbose && console.log(`Inferring types one more time (${i + 1} / ${options.inferTypes})...`);
233                scene.inferTypes();
234            }
235        }
236    }
237
238    if (options.entrypoint) {
239        if (options.verbose) {
240            console.log('Generating entrypoint...');
241        }
242        PointerAnalysis.pointerAnalysisForWholeProject(scene);
243    }
244
245    serializeScene(scene, outDir, options.verbose);
246
247    if (options.verbose) {
248        console.log('All done!');
249    }
250}
251
252function myParseInt(value: string, _previous: number): number {
253    const parsedValue = parseInt(value, 10);
254    if (isNaN(parsedValue)) {
255        throw new InvalidArgumentError('Must be a number.');
256    }
257    if (parsedValue < 1) {
258        throw new InvalidArgumentError('Must be greater than 0.');
259    }
260    return parsedValue;
261}
262
263export const program = new Command()
264    .name('serializeArkIR')
265    .description('Serialize ArkIR for TypeScript files or projects to JSON')
266    .argument('<input>', 'Input file or directory')
267    .argument('<output>', 'Output file or directory')
268    .option('-m, --multi', 'Flag to indicate the input is a directory', false)
269    .option('-p, --project', 'Flag to indicate the input is a project directory', false)
270    .option('-t, --infer-types [times]', 'Infer types in the ArkIR', myParseInt)
271    .option('-e, --entrypoint', 'Generate entrypoint for the files', false)
272    .option('-v, --verbose', 'Verbose output', false)
273    .action((input: any, output: any, options: any) => {
274        // Check for invalid combinations of flags
275        if (options.multi && options.project) {
276            console.error(`ERROR: You cannot provide both the '-m' and '-p' flags.`);
277            process.exit(1);
278        }
279
280        // Ensure the input path exists
281        if (!fs.existsSync(input)) {
282            console.error(`ERROR: The input path '${input}' does not exist.`);
283            process.exit(1);
284        }
285
286        // Handle the case where the input is a directory
287        if (fs.statSync(input).isDirectory() && !(options.multi || options.project)) {
288            console.error(`ERROR: If the input is a directory, you must provide the '-p' or '-m' flag.`);
289            process.exit(1);
290        }
291
292        if (options.project) {
293            serializeTsProject(input, output, options);
294        } else if (options.multi) {
295            serializeMultipleTsFiles(input, output, options);
296        } else {
297            serializeSingleTsFile(input, output, options);
298        }
299    });
300
301if (require.main === module) {
302    program.parse(process.argv);
303}
304