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 { SyntaxKind } from 'typescript';
18import type { SourceFile } from 'typescript';
19import { firstCharacterToUppercase } from '../common/commonUtils';
20import type { ModuleBlockEntity } from '../declaration-node/moduleDeclaration';
21import {
22  getDefaultExportClassDeclaration,
23  getSourceFileFunctions,
24  getSourceFileVariableStatements
25} from '../declaration-node/sourceFileElementsAssemply';
26import { generateClassDeclaration } from './generateClassDeclaration';
27import { generateCommonFunction } from './generateCommonFunction';
28import { generateEnumDeclaration } from './generateEnumDeclaration';
29import { generateImportEqual } from './generateImportEqual';
30import { addToIndexArray } from './generateIndex';
31import { generateInterfaceDeclaration } from './generateInterfaceDeclaration';
32import { generateStaticFunction } from './generateStaticFunction';
33import { addToSystemIndexArray } from './generateSystemIndex';
34import { generateTypeAliasDeclaration } from './generateTypeAlias';
35import { generateVariableStatementDelcatation } from './generateVariableStatementDeclaration';
36import type { ImportElementEntity } from '../declaration-node/importAndExportDeclaration';
37import { ClassEntity } from '../declaration-node/classDeclaration';
38
39interface ModuleExportEntity {
40  type: string;
41  name: string;
42}
43
44interface DefaultExportClassProps {
45  moduleBody: string;
46  outBody: string;
47  filename: string;
48  sourceFile: SourceFile;
49  mockApi: string;
50}
51
52interface DefaultExportClassBack {
53  moduleBody: string;
54  outBody: string;
55}
56
57interface JudgmentModuleEntityProps {
58  moduleEntity: ModuleBlockEntity;
59  moduleBody: string;
60  outBody: string;
61  enumBody: string;
62  sourceFile: SourceFile;
63  mockApi: string;
64  extraImport: string[];
65  moduleName: string;
66  importDeclarations: ImportElementEntity[];
67}
68
69interface JudgmentModuleEntityBack {
70  moduleBody: string;
71  outBody: string;
72  enumBody: string;
73}
74
75interface ModuleEntityLoopProps {
76  moduleEntity: ModuleBlockEntity;
77  innerOutBody: string;
78  moduleBody: string;
79  sourceFile: SourceFile;
80  mockApi: string;
81  extraImport: string[];
82  innerModuleName: string;
83  importDeclarations: ImportElementEntity[];
84}
85
86interface ModuleEntityLoopBack {
87  innerOutBody: string;
88  moduleBody: string;
89}
90
91interface ModuleEntityNextProps {
92  moduleEntity: ModuleBlockEntity;
93  innerFunctionBody: string;
94  innerModuleBody: string;
95  filename: string;
96  moduleBody: string;
97  sourceFile: SourceFile;
98  mockApi: string;
99  extraImport: string[];
100  innerModuleName: string;
101  importDeclarations: ImportElementEntity[];
102}
103
104interface ModuleEntityNextBack {
105  innerModuleName: string;
106  moduleBody: string;
107}
108
109/**
110 * generate declare
111 * @param moduleEntity
112 * @param sourceFile
113 * @param filename
114 * @param extraImport
115 * @returns
116 */
117export function generateModuleDeclaration(
118  moduleEntity: ModuleBlockEntity,
119  sourceFile: SourceFile,
120  filename: string,
121  mockApi: string,
122  extraImport: string[],
123  importDeclarations: ImportElementEntity[]
124): string {
125  const innerModuleBody = '';
126  const moduleName = moduleEntity.moduleName.replace(/["']/g, '');
127  let moduleBody = `export function mock${firstCharacterToUppercase(moduleName)}() {\n`;
128  let enumBody = '';
129  if (
130    !(
131      moduleEntity.exportModifiers.includes(SyntaxKind.DeclareKeyword) &&
132      (moduleEntity.moduleName.startsWith('"') || moduleEntity.moduleName.startsWith('\''))
133    ) &&
134    path.basename(sourceFile.fileName).startsWith('@ohos')
135  ) {
136    addToIndexArray({ fileName: filename, mockFunctionName: `mock${firstCharacterToUppercase(moduleName)}` });
137  }
138  let outBody = '';
139  const defaultExportClassBack = defaultExportClassForEach({ moduleBody, outBody, filename, sourceFile, mockApi });
140  moduleBody = defaultExportClassBack.moduleBody;
141  outBody = defaultExportClassBack.outBody;
142  const judgmentModuleEntityProps = {
143    moduleEntity,
144    moduleBody: defaultExportClassBack.moduleBody,
145    outBody: defaultExportClassBack.outBody,
146    sourceFile,
147    mockApi,
148    enumBody,
149    extraImport,
150    moduleName,
151    importDeclarations
152  };
153  const judgmentModuleEntityBack = judgmentModuleEntity(judgmentModuleEntityProps);
154  moduleBody = judgmentModuleEntityBack.moduleBody;
155  outBody = judgmentModuleEntityBack.outBody;
156  enumBody = judgmentModuleEntityBack.enumBody;
157  moduleBody = moduleEntityForEach(judgmentModuleEntityProps, innerModuleBody, filename);
158
159  const exportString = getExportBody(moduleEntity);
160  if (exportString !== '') {
161    moduleBody += '\t' + exportString;
162  }
163
164  moduleBody += '\t};';
165  moduleBody += `\n\treturn ${moduleName};}\n`;
166  moduleBody += outBody;
167  moduleBody = enumBody + moduleBody;
168  return moduleBody;
169}
170
171/**
172 * Obtain the variables to be exported.
173 *
174 * @param moduleEntity ModuleBlockEntity
175 * @returns string
176 */
177function getExportBody(moduleEntity: ModuleBlockEntity): string {
178  let exportString = '';
179  const exports = getModuleExportElements(moduleEntity);
180  exports.forEach(value => {
181    if (value.type === 'module' && !value.name.startsWith("'") && !value.name.startsWith('"')) {
182      exportString += `${value.name}: mock${value.name}(),\n`;
183    } else {
184      exportString += `${value.name}: ${value.name},\n`;
185    }
186  });
187  return exportString;
188}
189
190/**
191 * judgment ModuleEntityLength
192 * @param props
193 * @param innerModuleBody
194 * @param filename
195 * @returns
196 */
197function moduleEntityForEach(props: JudgmentModuleEntityProps, innerModuleBody: string, filename: string): string {
198  let functionBody = '';
199  if (props.moduleEntity.functionDeclarations.size > 0) {
200    props.moduleEntity.functionDeclarations.forEach(value => {
201      functionBody += '\t' + generateCommonFunction(props.moduleName, value, props.sourceFile,
202        props.mockApi, false) + '\n';
203    });
204  }
205  if (props.moduleEntity.moduleDeclarations.length > 0) {
206    props.moduleEntity.moduleDeclarations.forEach(value => {
207      if (!value.moduleName.startsWith("'") && !value.moduleName.startsWith('"')) {
208        innerModuleBody += generateInnerModuleDeclaration(value, props.sourceFile, filename, props.mockApi,
209          props.extraImport, props.importDeclarations);
210      }
211    });
212  }
213  if (innerModuleBody) {
214    props.moduleBody += innerModuleBody + '\n';
215  }
216  props.moduleBody += '\t' + `const ${props.moduleName} = {`;
217  if (props.moduleEntity.variableStatements.length > 0) {
218    props.moduleEntity.variableStatements.forEach(value => {
219      value.forEach(val => {
220        props.moduleBody += generateVariableStatementDelcatation(val, false) + '\n';
221      });
222    });
223  }
224  const sourceFileFunctions = getSourceFileFunctions(props.sourceFile);
225  let sourceFileFunctionBody = '';
226  if (sourceFileFunctions.size > 0) {
227    sourceFileFunctions.forEach(value => {
228      sourceFileFunctionBody += '\n' + generateCommonFunction(props.moduleName, value,
229        props.sourceFile, props.mockApi, false);
230    });
231  }
232  const sourceFileVariableStatements = getSourceFileVariableStatements(props.sourceFile);
233  let sourceFileStatementBody = '';
234  if (sourceFileVariableStatements.length > 0) {
235    sourceFileVariableStatements.forEach(value => {
236      value.forEach(val => {
237        sourceFileStatementBody += '\n' + generateVariableStatementDelcatation(val, false);
238      });
239    });
240  }
241  props.moduleBody += sourceFileFunctionBody + '\n';
242  props.moduleBody += sourceFileStatementBody + '\n';
243  props.moduleBody += functionBody + '\n';
244  return props.moduleBody;
245}
246
247/**
248 * handle extra class declaration body
249 * @param value
250 * @param fileName
251 * @returns
252 */
253function handleExtraClassDeclarationBody(value: ClassEntity, fileName: string): boolean {
254  if (fileName.includes('@ohos.util.stream.d.ts') && value.className === 'Transform') {
255    return true;
256  }
257  return false;
258}
259
260/**
261 * judgment ModuleEntity
262 * @param props
263 * @returns
264 */
265function judgmentModuleEntity(props: JudgmentModuleEntityProps): JudgmentModuleEntityBack {
266  if (props.moduleEntity.typeAliasDeclarations.length > 0) {
267    props.moduleEntity.typeAliasDeclarations.forEach(value => {
268      props.outBody +=
269        generateTypeAliasDeclaration(value, true, props.sourceFile, props.extraImport, props.mockApi) + '\n';
270    });
271  }
272  if (props.moduleEntity.moduleImportEquaqls.length > 0) {
273    props.moduleEntity.moduleImportEquaqls.forEach(value => {
274      props.outBody += generateImportEqual(value) + '\n';
275    });
276  }
277  if (props.moduleEntity.classDeclarations.length > 0) {
278    props.outBody = generateClassDeclarations(props);
279  }
280  if (props.moduleEntity.interfaceDeclarations.length > 0) {
281    props.moduleEntity.interfaceDeclarations.forEach(value => {
282      props.outBody += generateInterfaceDeclaration(value, props.sourceFile, false, props.mockApi,
283        props.moduleEntity.interfaceDeclarations, props.importDeclarations, props.extraImport) + ';\n';
284    });
285  }
286  if (props.moduleEntity.enumDeclarations.length > 0) {
287    props.moduleEntity.enumDeclarations.forEach(value => {
288      props.enumBody += generateEnumDeclaration(props.moduleName, value) + '\n';
289    });
290  }
291  return {
292    outBody: props.outBody,
293    moduleBody: props.moduleBody,
294    enumBody: props.enumBody
295  };
296}
297
298/**
299 * generate classDeclarations
300 * @param props
301 * @returns
302 */
303function generateClassDeclarations(props: JudgmentModuleEntityProps): string {
304  let extraOutBody = '';
305  props.moduleEntity.classDeclarations.forEach(value => {
306    const body =
307      generateClassDeclaration(props.moduleName, value, false, '', '', props.sourceFile, false, props.mockApi) + '\n';
308    if (handleExtraClassDeclarationBody(value, props.sourceFile.fileName)) {
309      extraOutBody = body;
310    } else {
311      props.outBody += body;
312    }
313  });
314  props.outBody += extraOutBody;
315  return props.outBody;
316}
317
318/**
319 * defaultExportClass ForEach
320 * @param props
321 * @returns
322 */
323function defaultExportClassForEach(props: DefaultExportClassProps): DefaultExportClassBack {
324  const defaultExportClass = getDefaultExportClassDeclaration(props.sourceFile);
325
326  if (defaultExportClass.length > 0) {
327    defaultExportClass.forEach(value => {
328      if (
329        value.exportModifiers.includes(SyntaxKind.DefaultKeyword) &&
330        value.exportModifiers.includes(SyntaxKind.ExportKeyword)
331      ) {
332        const moduleBodyAndOutBodyBack = getModuleBodyAndOutBody(props, value);
333        props.outBody = moduleBodyAndOutBodyBack.outBody;
334        props.moduleBody = moduleBodyAndOutBodyBack.moduleBody;
335      }
336    });
337  }
338  return {
339    outBody: props.outBody,
340    moduleBody: props.moduleBody
341  };
342}
343
344/**
345 * get ModuleBodyAndOutBody
346 * @param props
347 * @param value
348 * @returns
349 */
350function getModuleBodyAndOutBody(props: DefaultExportClassProps, value: ClassEntity): DefaultExportClassBack {
351  if (props.filename.startsWith('system_')) {
352    const mockNameArr = props.filename.split('_');
353    const mockName = mockNameArr[mockNameArr.length - 1];
354    addToSystemIndexArray({
355      filename: props.filename,
356      mockFunctionName: `mock${firstCharacterToUppercase(mockName)}`
357    });
358
359    props.moduleBody += `global.systemplugin.${mockName} = {`;
360    if (value.staticMethods.length > 0) {
361      let staticMethodBody = '';
362      value.staticMethods.forEach(val => {
363        staticMethodBody += generateStaticFunction(val, true, props.sourceFile, props.mockApi) + '\n';
364      });
365      props.moduleBody += staticMethodBody;
366    }
367    props.moduleBody += '}';
368  } else {
369    props.outBody += generateClassDeclaration(
370      '',
371      value,
372      false,
373      '',
374      props.filename,
375      props.sourceFile,
376      false,
377      props.mockApi
378    );
379  }
380  return {
381    outBody: props.outBody,
382    moduleBody: props.moduleBody
383  };
384}
385
386function generateInnerModuleDeclaration(
387  moduleEntity: ModuleBlockEntity,
388  sourceFile: SourceFile,
389  filename: string,
390  mockApi: string,
391  extraImport: string[],
392  importDeclarations: ImportElementEntity[]
393): string {
394  const innerModuleBody = '';
395  let innerModuleName = moduleEntity.moduleName.replace(/["']/g, '');
396  let moduleBody = `function mock${innerModuleName}() {\n`;
397  let innerOutBody = '';
398  const innerFunctionBody = '';
399  const moduleEntityLoopBack = moduleEntityLoop({
400    moduleEntity,
401    innerOutBody,
402    moduleBody,
403    sourceFile,
404    mockApi,
405    extraImport,
406    innerModuleName,
407    importDeclarations
408  });
409  innerOutBody = moduleEntityLoopBack.innerOutBody;
410  moduleBody = moduleEntityLoopBack.moduleBody;
411  const moduleEntityNextBack = moduleEntityNext({
412    moduleEntity,
413    innerFunctionBody,
414    innerModuleBody,
415    filename,
416    moduleBody,
417    sourceFile,
418    mockApi,
419    extraImport,
420    innerModuleName,
421    importDeclarations
422  });
423  innerModuleName = moduleEntityNextBack.innerModuleName;
424  moduleBody = moduleEntityNextBack.moduleBody;
425  moduleBody += '\t};';
426  moduleBody += `\n\treturn ${innerModuleName};}\n`;
427  moduleBody += innerOutBody;
428  return moduleBody;
429}
430
431/**
432 * moduleEntity judgment
433 * @param props
434 * @returns
435 */
436function moduleEntityLoop(props: ModuleEntityLoopProps): ModuleEntityLoopBack {
437  if (props.moduleEntity.typeAliasDeclarations.length) {
438    props.moduleEntity.typeAliasDeclarations.forEach(value => {
439      props.innerOutBody +=
440        generateTypeAliasDeclaration(value, true, props.sourceFile, props.extraImport, props.mockApi) + '\n';
441    });
442  }
443  if (props.moduleEntity.moduleImportEquaqls.length) {
444    props.moduleEntity.moduleImportEquaqls.forEach(value => {
445      props.innerOutBody += generateImportEqual(value) + '\n';
446    });
447  }
448
449  if (props.moduleEntity.classDeclarations.length) {
450    props.moduleEntity.classDeclarations.forEach(value => {
451      if (value.exportModifiers.length && value.exportModifiers.includes(SyntaxKind.ExportKeyword)) {
452        props.innerOutBody += generateClassDeclaration(props.innerModuleName, value, false, '', '', props.sourceFile, false, props.mockApi) + '\n';
453      } else {
454        props.moduleBody += '\t' + generateClassDeclaration(props.innerModuleName, value, false, '', '', props.sourceFile, true, props.mockApi) + '\n';
455      }
456    });
457  }
458  if (props.moduleEntity.interfaceDeclarations.length) {
459    props.moduleEntity.interfaceDeclarations.forEach(value => {
460      if (value.exportModifiers.length) {
461        props.innerOutBody += generateInterfaceDeclaration(value, props.sourceFile, false, props.mockApi,
462          props.moduleEntity.interfaceDeclarations, props.importDeclarations, props.extraImport) + ';\n';
463      } else {
464        props.moduleBody += '\t' + generateInterfaceDeclaration(value, props.sourceFile, false, props.mockApi,
465          props.moduleEntity.interfaceDeclarations, props.importDeclarations, props.extraImport) + ';\n';
466      }
467    });
468  }
469  if (props.moduleEntity.enumDeclarations.length) {
470    props.moduleEntity.enumDeclarations.forEach(value => {
471      if (value.exportModifiers.length) {
472        props.innerOutBody += generateEnumDeclaration(props.innerModuleName, value) + '\n';
473      } else {
474        props.moduleBody += generateEnumDeclaration(props.innerModuleName, value);
475      }
476    });
477  }
478  return {
479    moduleBody: props.moduleBody,
480    innerOutBody: props.innerOutBody
481  };
482}
483
484/**
485 * Next moduleEntity judgment
486 * @param props
487 * @returns
488 */
489function moduleEntityNext(props: ModuleEntityNextProps): ModuleEntityNextBack {
490  if (props.moduleEntity.functionDeclarations.size) {
491    props.moduleEntity.functionDeclarations.forEach(value => {
492      props.innerFunctionBody += '\n' + generateCommonFunction(props.innerModuleName, value,
493        props.sourceFile, props.mockApi, false) + '\n';
494    });
495  }
496
497  if (props.moduleEntity.moduleDeclarations.length) {
498    props.moduleEntity.moduleDeclarations.forEach(value => {
499      if (!value.moduleName.startsWith("'") && !value.moduleName.startsWith('"')) {
500        props.innerModuleBody += generateInnerModuleDeclaration(value, props.sourceFile, props.filename,
501          props.mockApi, props.extraImport, props.importDeclarations);
502      }
503    });
504  }
505  if (props.innerModuleBody) {
506    props.moduleBody += props.innerModuleBody + '\n';
507  }
508
509  props.moduleBody += `const ${props.innerModuleName} = {\n`;
510  if (props.moduleEntity.variableStatements.length) {
511    props.moduleEntity.variableStatements.forEach(value => {
512      value.forEach(val => {
513        props.moduleBody += generateVariableStatementDelcatation(val, false) + '\n';
514      });
515    });
516  }
517
518  props.moduleBody += props.innerFunctionBody + '\n';
519
520  const exportArr = getModuleExportElements(props.moduleEntity);
521  let innerExportString = '';
522  exportArr.forEach(value => {
523    if (value.type === 'module' && !value.name.startsWith("'") && !value.name.startsWith('"')) {
524      innerExportString += `${value.name}: mock${value.name}(),\n`;
525    } else {
526      innerExportString += `${value.name}: ${value.name},\n`;
527    }
528  });
529  if (innerExportString !== '') {
530    props.moduleBody += '\t' + innerExportString;
531  }
532  return {
533    innerModuleName: props.innerModuleName,
534    moduleBody: props.moduleBody
535  };
536}
537
538/**
539 * generate inner module for declare module
540 * @param moduleEntity
541 * @returns
542 */
543function generateInnerDeclareModule(moduleEntity: ModuleBlockEntity): string {
544  const moduleName = '$' + moduleEntity.moduleName.replace(/["']/g, '');
545  let module = `\n\texport const ${moduleName} = `;
546  if (moduleEntity.exportDeclarations.length > 0) {
547    moduleEntity.exportDeclarations.forEach(value => {
548      module += value.match(/{[^{}]*}/g)[0] + '\n';
549    });
550  }
551  return module;
552}
553
554/**
555 * generate inner module
556 * @param moduleEntity
557 * @param sourceFile
558 * @param extraImport
559 * @returns
560 */
561function generateInnerModule(
562  moduleEntity: ModuleBlockEntity,
563  sourceFile: SourceFile,
564  extraImport: string[],
565  mockApi: string
566): string {
567  const moduleName = moduleEntity.moduleName;
568  let innerModuleBody = `const ${moduleName} = (()=> {`;
569
570  if (moduleEntity.enumDeclarations.length > 0) {
571    moduleEntity.enumDeclarations.forEach(value => {
572      innerModuleBody += generateEnumDeclaration(moduleName, value) + '\n';
573    });
574  }
575
576  if (moduleEntity.typeAliasDeclarations.length > 0) {
577    moduleEntity.typeAliasDeclarations.forEach(value => {
578      innerModuleBody += generateTypeAliasDeclaration(value, true, sourceFile, extraImport, mockApi) + '\n';
579    });
580  }
581
582  if (moduleEntity.moduleImportEquaqls.length > 0) {
583    moduleEntity.moduleImportEquaqls.forEach(value => {
584      innerModuleBody += generateImportEqual(value) + '\n';
585    });
586  }
587
588  if (moduleEntity.interfaceDeclarations.length > 0) {
589    moduleEntity.interfaceDeclarations.forEach(value => {
590      innerModuleBody += generateInterfaceDeclaration(value, sourceFile, false, '', moduleEntity.interfaceDeclarations) + '\n';
591    });
592  }
593
594  let functionBody = 'return {';
595  if (moduleEntity.functionDeclarations.size > 0) {
596    moduleEntity.functionDeclarations.forEach(value => {
597      functionBody += generateCommonFunction(moduleName, value, sourceFile, '', false) + '\n';
598    });
599  }
600
601  if (moduleEntity.variableStatements.length > 0) {
602    moduleEntity.variableStatements.forEach(value => {
603      value.forEach(val => {
604        innerModuleBody += generateVariableStatementDelcatation(val, true) + '\n';
605      });
606    });
607  }
608  innerModuleBody += functionBody + '\n';
609
610  const exports = getModuleExportElements(moduleEntity);
611  let exportString = '';
612  exports.forEach(value => {
613    exportString += `${value.name}: ${value.name},\n`;
614  });
615  if (exportString !== '') {
616    innerModuleBody += '\t' + exportString;
617  }
618  innerModuleBody += '\t};})();';
619  return innerModuleBody;
620}
621
622/**
623 * get all export elements
624 * @param moduleEntity
625 * @returns
626 */
627function getModuleExportElements(moduleEntity: ModuleBlockEntity): Array<ModuleExportEntity> {
628  const exportElements: Array<ModuleExportEntity> = [];
629  if (moduleEntity.moduleName.startsWith('"') && moduleEntity.moduleName.endsWith('"')) {
630    return exportElements;
631  }
632  if (moduleEntity.classDeclarations.length > 0) {
633    moduleEntity.classDeclarations.forEach(value => {
634      exportElements.push({ name: firstCharacterToUppercase(value.className), type: 'class' });
635    });
636  }
637
638  if (moduleEntity.interfaceDeclarations.length > 0) {
639    moduleEntity.interfaceDeclarations.forEach(value => {
640      exportElements.push({ name: value.interfaceName, type: 'interface' });
641    });
642  }
643
644  if (moduleEntity.enumDeclarations.length > 0) {
645    moduleEntity.enumDeclarations.forEach(value => {
646      exportElements.push({ name: value.enumName, type: 'enum' });
647    });
648  }
649
650  if (moduleEntity.moduleDeclarations.length > 0) {
651    moduleEntity.moduleDeclarations.forEach(value => {
652      exportElements.push({ name: value.moduleName, type: 'module' });
653    });
654  }
655
656  if (moduleEntity.typeAliasDeclarations.length > 0) {
657    moduleEntity.typeAliasDeclarations.forEach(value => {
658      exportElements.push({ name: value.typeAliasName, type: 'type' });
659    });
660  }
661  return exportElements;
662}
663