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