1/* 2 * Copyright (c) 2023 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 16const { Project, Sdk, FileSystem, Logger } = require('./utils'); 17const { ApiWriter, ApiExcelWriter } = require('./api_writer'); 18const { SystemApiRecognizer } = require('./api_recognizer'); 19const { ReporterFormat } = require('./configs'); 20const ts = require('typescript'); 21const fs = require('fs'); 22 23class ProgramFactory { 24 setLibPath(libPath) { 25 this.libPath = libPath; 26 } 27 28 getETSOptions(componentLibs) { 29 const tsconfig = require('../tsconfig.json'); 30 const etsConfig = tsconfig.compilerOptions.ets; 31 etsConfig.libs = [...componentLibs]; 32 return etsConfig; 33 } 34 35 createProgram(rootNames, apiLibs, componentLibs, esLibs) { 36 const compilerOption = { 37 target: ts.ScriptTarget.ES2017, 38 ets: this.getETSOptions([]), 39 allowJs: false, 40 lib: [...apiLibs, ...componentLibs, ...esLibs], 41 module: ts.ModuleKind.CommonJS, 42 }; 43 this.compilerHost = this.createCompilerHost({ 44 resolveModuleName: (moduleName) => { 45 return this.resolveModuleName(moduleName, apiLibs); 46 }, 47 }, compilerOption); 48 49 if (this.libPath && fs.existsSync(this.libPath)) { 50 Logger.info('ProgramFactory', `set default lib location: ${this.libPath}`); 51 this.compilerHost.getDefaultLibLocation = () => { 52 return this.libPath; 53 }; 54 } 55 return ts.createProgram({ 56 rootNames: [...rootNames], 57 options: compilerOption, 58 host: this.compilerHost, 59 }); 60 } 61 62 resolveModuleName(moduleName, libs) { 63 if (moduleName.startsWith('@')) { 64 const moduleFileName = `${moduleName}.d.ts`; 65 for (const lib of libs) { 66 if (lib.endsWith(moduleFileName)) { 67 return lib; 68 } 69 } 70 } 71 return undefined; 72 } 73 74 createCompilerHost(moduleResolver, compilerOption) { 75 const compilerHost = ts.createCompilerHost(compilerOption); 76 compilerHost.resolveModuleNames = this.getResolveModuleNames(moduleResolver); 77 return compilerHost; 78 } 79 80 getResolveModuleNames(moduleResolver) { 81 return (moduleNames, containingFile, reusedNames, redirectedReference, options) => { 82 const resolvedModules = []; 83 for (const moduleName of moduleNames) { 84 const moduleLookupLocaton = ts.resolveModuleName(moduleName, containingFile, options, { 85 fileExists: (fileName) => { 86 return fileName && ts.sys.fileExists(fileName); 87 }, 88 readFile: (fileName) => { 89 ts.sys.readFile(fileName); 90 }, 91 }); 92 if (moduleLookupLocaton.resolvedModule) { 93 resolvedModules.push(moduleLookupLocaton.resolvedModule); 94 } else { 95 const modulePath = moduleResolver.resolveModuleName(moduleName); 96 const resolved = modulePath && ts.sys.fileExists(modulePath) ? { resolvedFileName: modulePath } : undefined; 97 resolvedModules.push(resolved); 98 } 99 } 100 return resolvedModules; 101 }; 102 } 103} 104 105class ApiCollector { 106 constructor(argv) { 107 const appProject = argv.app ? argv.app : (argv.dir ? argv.dir : undefined); 108 if (!appProject) { 109 throw 'app not found'; 110 } 111 this.project = new Project(appProject, argv.dir !== undefined); 112 this.sdk = new Sdk(this.project, argv.sdk, argv.sdkRoot); 113 this.formatFlag = ReporterFormat.getFlag(argv.format); 114 this.outputPath = !argv.output ? appProject : argv.output; 115 this.logTag = 'ApiCollector'; 116 this.debugFlag = argv.debug; 117 } 118 119 setLibPath(libPath) { 120 this.libPath = libPath; 121 if (libPath && !fs.existsSync(this.libPath)) { 122 Logger.warn(this.logTag, `${libPath} is not exist`); 123 } else { 124 Logger.info(this.logTag, `set lib path ${libPath}`); 125 } 126 return this; 127 } 128 129 setIncludeTest(isIncludeTest) { 130 this.isIncludeTest = isIncludeTest; 131 return this; 132 } 133 134 async start() { 135 const sdkPath = this.sdk.getPath(); 136 if (!sdkPath || !fs.existsSync(sdkPath)) { 137 return; 138 } 139 Logger.info(this.logTag, `scan app ${this.project.getPath()}`); 140 Logger.info(this.logTag, `sdk is in ${sdkPath}`); 141 const apiLibs = this.sdk.getApiLibs(); 142 const componentLibs = this.sdk.getComponentLibs(); 143 const eslibs = this.sdk.getESLibs(this.libPath); 144 const appSourceSet = this.project.getAppSources(this.isIncludeTest); 145 const programFactory = new ProgramFactory(); 146 programFactory.setLibPath(this.libPath); 147 let program = programFactory.createProgram(appSourceSet, apiLibs, componentLibs, eslibs); 148 149 if (this.debugFlag) { 150 program.getSourceFiles().forEach((sf) => { 151 Logger.info('ApiCollector', sf.fileName); 152 }); 153 } 154 155 let systemApiRecognizer = new SystemApiRecognizer(sdkPath); 156 systemApiRecognizer.setTypeChecker(program.getTypeChecker()); 157 Logger.info(this.logTag, `start scanning ${this.project.getPath()}`); 158 appSourceSet.forEach((appCodeFilePath) => { 159 const canonicalFileName = programFactory.compilerHost.getCanonicalFileName(appCodeFilePath); 160 const sourceFile = program.getSourceFileByPath(canonicalFileName); 161 if (sourceFile) { 162 if (this.debugFlag) { 163 Logger.info(this.logTag, `scan ${sourceFile.fileName}`); 164 } 165 systemApiRecognizer.visitNode(sourceFile, sourceFile.fileName); 166 } else { 167 Logger.warn(this.logTag, `no sourceFile ${appCodeFilePath}`); 168 } 169 }); 170 Logger.info(this.logTag, `end scan ${this.project.getPath()}`); 171 const apiWriter = this.getApiWriter(); 172 apiWriter.add(systemApiRecognizer.getApiInformations()); 173 // avoid oom 174 systemApiRecognizer = undefined; 175 program = undefined; 176 await apiWriter.flush(); 177 } 178 179 getApiWriter() { 180 if (!this.apiWriter) { 181 this.apiWriter = new ApiWriter(this.outputPath, this.formatFlag); 182 } 183 return this.apiWriter; 184 } 185 186 setApiWriter(apiWriter) { 187 this.apiWriter = apiWriter; 188 } 189} 190 191class MultiProjectApiCollector { 192 constructor(argv) { 193 this.argv = argv; 194 } 195 196 setLibPath(libPath) { 197 this.libPath = libPath; 198 if (libPath && !fs.existsSync(this.libPath)) { 199 Logger.warn(this.logTag, `${libPath} is not exist`); 200 } else { 201 Logger.info(this.logTag, `set lib path ${libPath}`); 202 } 203 return this; 204 } 205 206 setIncludeTest(isIncludeTest) { 207 this.isIncludeTest = isIncludeTest; 208 return this; 209 } 210 211 async start() { 212 const allApps = FileSystem.listAllAppDirs(this.argv.appDir); 213 if (allApps.length === 0) { 214 Logger.info('MultiProjectApiCollector', `project not found in ${this.argv.appDir}`); 215 return; 216 } 217 const output = !this.argv.output ? this.argv.appDir : this.argv.output; 218 const apiExcelWriter = new ApiExcelWriter(output); 219 apiExcelWriter.close(); 220 allApps.forEach((app) => { 221 if (app) { 222 this.argv.app = app; 223 const apiCollector = new ApiCollector(this.argv); 224 apiCollector.setApiWriter(apiExcelWriter); 225 apiCollector.setLibPath(this.libPath).setIncludeTest(this.isIncludeTest).start(); 226 } 227 }); 228 apiExcelWriter.open(); 229 await apiExcelWriter.flush(); 230 } 231} 232 233exports.ApiCollector = ApiCollector; 234exports.MultiProjectApiCollector = MultiProjectApiCollector;