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