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
16import path from 'path';
17import type {
18  CallSignatureDeclaration,
19  ComputedPropertyName,
20  FunctionDeclaration,
21  Identifier,
22  MethodDeclaration,
23  MethodSignature,
24  ModifiersArray,
25  ModuleDeclaration,
26  ParameterDeclaration,
27  PropertyName,
28  SourceFile
29} from 'typescript';
30import {
31  isClassDeclaration,
32  isComputedPropertyName,
33  isIdentifier,
34  isModuleBlock,
35  isModuleDeclaration,
36  isPrivateIdentifier
37} from 'typescript';
38import fs from 'fs';
39import ts from 'typescript';
40import type { ImportElementEntity } from '../declaration-node/importAndExportDeclaration';
41import { collectAllKitFiles } from './kitUtils';
42
43const paramIndex = 2;
44const allLegalImports = new Set<string>();
45const fileNameList = new Set<string>();
46const allClassSet = new Set<string>();
47
48export const dtsFileList: Array<string> = [];
49
50/**
51 * get all legal imports
52 * @returns
53 */
54export function getAllLegalImports(): Set<string> {
55  return new Set<string>(allLegalImports);
56}
57
58/**
59 * get all legal imports
60 * @param element
61 */
62export function collectAllLegalImports(element: string): void {
63  allLegalImports.add(element);
64}
65
66/**
67 * collect all mock js file path
68 * @returns
69 */
70export function getAllFileNameList(): Set<string> {
71  return new Set<string>(fileNameList);
72}
73
74/**
75 * collect all file name
76 */
77export function collectAllFileName(filePath: string): void {
78  const fullFileName = path.basename(filePath);
79  let fileName = '';
80  if (fullFileName.endsWith('d.ts')) {
81    fileName = fullFileName.split('.d.ts')[0];
82  } else if (fullFileName.endsWith('d.ets')) {
83    fileName = fullFileName.split('.d.ets')[0];
84  }
85
86  let outputFileName = '';
87  if (fileName.includes('@')) {
88    outputFileName = fileName.split('@')[1].replace(/\./g, '_');
89  } else {
90    outputFileName = fileName;
91  }
92  fileNameList.add(outputFileName);
93}
94
95/**
96 * get all class name set
97 * @returns
98 */
99export function getClassNameSet(): Set<string> {
100  return new Set<string>(allClassSet);
101}
102
103/**
104 * get all class declaration
105 * @param sourceFile
106 * @returns
107 */
108export function getAllClassDeclaration(sourceFile: SourceFile): Set<string> {
109  sourceFile.forEachChild(node => {
110    if (isClassDeclaration(node)) {
111      if (node.name !== undefined) {
112        allClassSet.add(node.name.escapedText.toString());
113      }
114    } else if (isModuleDeclaration(node)) {
115      const moduleDeclaration = node as ModuleDeclaration;
116      const moduleBody = moduleDeclaration.body;
117      parseModuleBody(moduleBody);
118    }
119  });
120  return allClassSet;
121}
122
123/**
124 * get module class declaration
125 * @param moduleBody
126 * @returns
127 */
128function parseModuleBody(moduleBody: ts.ModuleBody): void {
129  if (moduleBody !== undefined && isModuleBlock(moduleBody)) {
130    moduleBody.statements.forEach(value => {
131      if (isClassDeclaration(value) && value.name !== undefined) {
132        allClassSet.add(firstCharacterToUppercase(value.name?.escapedText.toString()));
133      }
134    });
135  }
136}
137
138/**
139 * get keywords
140 * @param modifiers
141 * @returns
142 */
143export function getModifiers(modifiers: ModifiersArray): Array<number> {
144  const modifiersArray: Array<number> = [];
145  modifiers.forEach(value => modifiersArray.push(value.kind));
146  return modifiersArray;
147}
148
149/**
150 * get property name
151 * @param node property node
152 * @param sourceFile
153 * @returns
154 */
155export function getPropertyName(node: PropertyName, sourceFile: SourceFile): string {
156  let propertyName = '';
157  const fileText = sourceFile.getFullText();
158  if (isIdentifier(node) || isPrivateIdentifier(node)) {
159    const newNameNode = node as Identifier;
160    propertyName = newNameNode.escapedText.toString();
161  } else if (isComputedPropertyName(node)) {
162    const newNameNode = node as ComputedPropertyName;
163    propertyName = fileText.slice(newNameNode.expression.pos, newNameNode.expression.end).trim();
164  } else {
165    propertyName = fileText.slice(node.pos, node.end).trim();
166  }
167  return propertyName;
168}
169
170/**
171 * get parameter declaration
172 * @param parameter
173 * @param sourceFile
174 * @returns
175 */
176export function getParameter(parameter: ParameterDeclaration, sourceFile: SourceFile): ParameterEntity {
177  let paramName = '';
178  let paramTypeString = '';
179  const paramTypeKind = parameter.type?.kind === undefined ? -1 : parameter.type.kind;
180  const fileText = sourceFile.getFullText();
181  if (isIdentifier(parameter.name)) {
182    paramName = parameter.name.escapedText === undefined ? '' : parameter.name.escapedText.toString();
183  } else {
184    const start = parameter.name.pos === undefined ? 0 : parameter.name.pos;
185    const end = parameter.name.end === undefined ? 0 : parameter.name.end;
186    paramName = fileText.slice(start, end).trim();
187  }
188
189  const start = parameter.type?.pos === undefined ? 0 : parameter.type.pos;
190  const end = parameter.type?.end === undefined ? 0 : parameter.type.end;
191  paramTypeString = fileText.slice(start, end).trim();
192  return {
193    paramName: paramName,
194    paramTypeString: paramTypeString,
195    paramTypeKind: paramTypeKind
196  };
197}
198
199/**
200 * get method or function return info
201 * @param node
202 * @param sourceFile
203 * @returns
204 */
205export function getFunctionAndMethodReturnInfo(
206  node: FunctionDeclaration | MethodDeclaration | MethodSignature | CallSignatureDeclaration,
207  sourceFile: SourceFile
208): ReturnTypeEntity {
209  const returnInfo = { returnKindName: '', returnKind: -1 };
210  if (node.type !== undefined) {
211    const start = node.type.pos === undefined ? 0 : node.type.pos;
212    const end = node.type.end === undefined ? 0 : node.type.end;
213    returnInfo.returnKindName = sourceFile.text.substring(start, end).trim();
214    returnInfo.returnKind = node.type.kind;
215  }
216  return returnInfo;
217}
218
219/**
220 * get export modifiers
221 * @param modifiers
222 * @returns
223 */
224export function getExportKeyword(modifiers: ModifiersArray): Array<number> {
225  const modifiersArray: Array<number> = [];
226  modifiers.forEach(value => {
227    modifiersArray.push(value.kind);
228  });
229  return modifiersArray;
230}
231
232/**
233 *
234 * @param str first letter capitalization
235 * @returns
236 */
237export function firstCharacterToUppercase(str: string): string {
238  return str.slice(0, 1).toUpperCase() + str.slice(1);
239}
240
241/**
242 * parameters entity
243 */
244export interface ParameterEntity {
245  paramName: string;
246  paramTypeString: string;
247  paramTypeKind: number;
248}
249
250/**
251 * return type entity
252 */
253export interface ReturnTypeEntity {
254  returnKindName: string;
255  returnKind: number;
256}
257
258/**
259 * Get OpenHarmony project dir
260 * @return project dir
261 */
262
263export function getProjectDir(): string {
264  const apiInputPath = process.argv[paramIndex];
265  const privateInterface = path.join('vendor', 'huawei', 'interface', 'hmscore_sdk_js', 'api');
266  const openInterface = path.join('interface', 'sdk-js', 'api');
267  if (apiInputPath.indexOf(openInterface) > -1) {
268    return apiInputPath.replace(`${path.sep}${openInterface}`, '');
269  } else {
270    return apiInputPath.replace(`${path.sep}${privateInterface}`, '');
271  }
272}
273
274/**
275 * return interface api dir in OpenHarmony
276 */
277export function getOhosInterfacesDir(): string {
278  return path.join(getProjectDir(), 'interface', 'sdk-js', 'api');
279}
280
281/**
282 * return interface api root path
283 * @returns apiInputPath
284 */
285export function getApiInputPath(): string {
286  return process.argv[paramIndex];
287}
288
289/**
290 * return OpenHarmony file path dependent on by HarmonyOs
291 * @param importPath path of depend imported
292 * @param sourceFile sourceFile of current file
293 * @returns dependsFilePath
294 */
295export function findOhosDependFile(importPath: string, sourceFile: SourceFile): string {
296  const interFaceDir = getOhosInterfacesDir();
297  const tmpImportPath = importPath.replace(/'/g, '').replace('.d.ts', '').replace('.d.ets', '');
298  const sourceFileDir = path.dirname(sourceFile.fileName);
299  let dependsFilePath: string;
300  if (tmpImportPath.startsWith('./')) {
301    const subIndex = 2;
302    dependsFilePath = path.join(sourceFileDir, tmpImportPath.substring(subIndex));
303  } else if (tmpImportPath.startsWith('../')) {
304    const backSymbolList = tmpImportPath.split('/').filter(step => step === '..');
305    dependsFilePath = [
306      ...sourceFileDir.split(path.sep).slice(0, -backSymbolList.length),
307      ...tmpImportPath.split('/').filter(step => step !== '..')
308    ].join(path.sep);
309  } else if (tmpImportPath.startsWith('@ohos.inner.')) {
310    const pathSteps = tmpImportPath.replace(/@ohos\.inner\./g, '').split('.');
311    for (let i = 0; i < pathSteps.length; i++) {
312      const tmpInterFaceDir = path.join(interFaceDir, ...pathSteps.slice(0, i), pathSteps.slice(i).join('.'));
313      if (fs.existsSync(tmpInterFaceDir + '.d.ts')) {
314        return tmpInterFaceDir + '.d.ts';
315      }
316
317      if (fs.existsSync(tmpInterFaceDir + '.d.ets')) {
318        return tmpInterFaceDir + '.d.ets';
319      }
320    }
321  } else if (tmpImportPath.startsWith('@ohos.')) {
322    dependsFilePath = path.join(getOhosInterfacesDir(), tmpImportPath);
323  }
324
325  if (fs.existsSync(dependsFilePath + '.d.ts')) {
326    return dependsFilePath + '.d.ts';
327  }
328
329  if (fs.existsSync(dependsFilePath + '.d.ets')) {
330    return dependsFilePath + '.d.ets';
331  }
332
333  console.warn(`Cannot find module '${importPath}'`);
334  return '';
335}
336
337/**
338 * Determine if the file is a openHarmony interface file
339 * @param path: interface file path
340 * @returns
341 */
342export function isOhosInterface(path: string): boolean {
343  return path.startsWith(getOhosInterfacesDir());
344}
345
346/**
347 * reutn js-sdk root folder full path
348 * @returns
349 */
350export function getJsSdkDir(): string {
351  let sdkJsDir = process.argv[paramIndex].split(path.sep).slice(0, -1).join(path.sep);
352  sdkJsDir += sdkJsDir.endsWith(path.sep) ? '' : path.sep;
353  return sdkJsDir;
354}
355
356/**
357 * Determine whether the object has been imported
358 * @param importDeclarations imported Declaration list in current file
359 * @param typeName Object being inspected
360 * @returns
361 */
362export function hasBeenImported(importDeclarations: ImportElementEntity[], typeName: string): boolean {
363  if (!typeName.trim()) {
364    return true;
365  }
366  if (isFirstCharLowerCase(typeName)) {
367    return true;
368  }
369  return importDeclarations.some(importDeclaration => {
370    if (importDeclaration.importElements.includes(typeName) && importDeclaration.importPath.includes('./')) {
371      return true;
372    }
373    return false;
374  });
375}
376
377/**
378 * Determine whether the first character in a string is a lowercase letter
379 * @param str target string
380 * @returns
381 */
382function isFirstCharLowerCase(str: string): boolean {
383  const lowerCaseFirstChar = str[0].toLowerCase();
384  return str[0] === lowerCaseFirstChar;
385}
386
387export const specialFiles = [
388  '@internal/component/ets/common.d.ts',
389  '@internal/component/ets/units.d.ts',
390  '@internal/component/ets/common_ts_ets_api.d.ts',
391  '@internal/component/ets/enums.d.ts',
392  '@internal/component/ets/alert_dialog.d.ts',
393  '@internal/component/ets/ability_component.d.ts',
394  '@internal/component/ets/rich_editor.d.ts',
395  '@internal/component/ets/symbolglyph.d.ts',
396  '@internal/component/ets/button.d.ts',
397  '@internal/component/ets/nav_destination.d.ts',
398  '@internal/component/ets/navigation.d.ts',
399  '@internal/component/ets/text_common.d.ts',
400  '@internal/component/ets/styled_string.d.ts'
401];
402
403export const specialType = ['Storage', 'File', 'ChildProcess', 'Cipher', 'Sensor', 'Authenticator'];
404
405export const specialClassName = ['Want', 'Configuration', 'InputMethodExtensionContext'];
406
407/**
408 * get add kit file map
409 * @param apiInputPath api input path
410 * @returns
411 */
412export function generateKitMap(apiInputPath: string): void {
413  const kitPath = path.join(apiInputPath, '../', 'kits');
414  if (!fs.existsSync(kitPath)) {
415    throw new Error(`${kitPath} does not exist.`);
416  }
417  collectAllKitFiles(kitPath);
418}
419
420export interface DependencyListParams {
421  dependency: Array<string>;
422  export: string;
423}
424
425export interface DependencyParams {
426  [key: string]: DependencyListParams;
427}
428
429// dependence on collecting files
430export const DEPENDENCY_LIST: DependencyParams = {};
431
432// Json file indentation configuration
433export const JSON_FILE_INDENTATION = 2;
434
435/**
436 * generated depend.json
437 */
438export function generateDependJsonFile(): void {
439  const dependInfoPath = path.join(__dirname, '../../../runtime/main/extend/systemplugin/depend.json');
440  fs.writeFileSync(dependInfoPath, JSON.stringify(DEPENDENCY_LIST, null, JSON_FILE_INDENTATION), 'utf-8');
441}
442
443/**
444 * generated MyComponent.js
445 *
446 * @param outDir generated file root directory
447 */
448export function generateMyComponent(outDir: string): void {
449  fs.writeFileSync(path.join(outDir, 'MyComponent.js'), 'class MyComponent {}\nexport { MyComponent };');
450}
451
452// initialize all variables in the file
453export let INITVARIABLE = '';
454
455/**
456 * set initialize variable
457 *
458 * @param value variable name
459 */
460export function setInitVariable(value?: string): void {
461  if (value) {
462    if (!INITVARIABLE.includes(`let ${value} = {};`)) {
463      INITVARIABLE += `let ${value} = {};\n`;
464    }
465  } else {
466    INITVARIABLE = '';
467  }
468}
469
470/**
471 * get all initialize variable
472 * @returns string
473 */
474export function getInitVariable(): string {
475  return INITVARIABLE;
476}
477