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 { SourceFile } from 'typescript';
18import { SyntaxKind } from 'typescript';
19import { firstCharacterToUppercase } from '../common/commonUtils';
20import type { ClassEntity } from '../declaration-node/classDeclaration';
21import { generateCommonMethod } from './generateCommonMethod';
22import { getWarnConsole } from './generateCommonUtil';
23import { generatePropertyDeclaration } from './generatePropertyDeclaration';
24import { generateStaticFunction } from './generateStaticFunction';
25import { ImportElementEntity } from '../declaration-node/importAndExportDeclaration';
26import { HeritageClauseEntity } from '../declaration-node/heritageClauseDeclaration';
27
28interface AssemblyClassParams {
29  isSystem: boolean;
30  classEntity: ClassEntity;
31  classBody: string;
32  sourceFile: SourceFile;
33  mockApi: string;
34  isInnerMockFunction: boolean;
35  filename: string;
36  isExtend: boolean;
37  className: string;
38  extraImport?: string[];
39  importDeclarations?: ImportElementEntity[];
40}
41
42/**
43 * generate class
44 * @param rootName
45 * @param classEntity
46 * @param isSystem
47 * @param globalName
48 * @param filename
49 * @param sourceFile
50 * @param isInnerMockFunction
51 * @returns
52 */
53export function generateClassDeclaration(
54  rootName: string,
55  classEntity: ClassEntity,
56  isSystem: boolean,
57  globalName: string,
58  filename: string,
59  sourceFile: SourceFile,
60  isInnerMockFunction: boolean,
61  mockApi: string,
62  extraImport?: string[],
63  importDeclarations?: ImportElementEntity[]
64): string {
65  if (isSystem) {
66    return '';
67  }
68
69  const className = firstCharacterToUppercase(classEntity.className);
70  let classBody = '';
71  if (
72    (classEntity.exportModifiers.includes(SyntaxKind.ExportKeyword) ||
73      classEntity.exportModifiers.includes(SyntaxKind.DeclareKeyword)) &&
74    !isInnerMockFunction
75  ) {
76    classBody += `export const ${className} = class ${className} `;
77  } else {
78    classBody += `const ${className} = class ${className} `;
79  }
80
81  const heritageClausesData = handleClassEntityHeritageClauses(rootName, classEntity, mockApi, sourceFile);
82  const isExtend = heritageClausesData.isExtend;
83  classBody = addCustomeClass(heritageClausesData, sourceFile, importDeclarations) + classBody;
84  classBody += heritageClausesData.classBody;
85  classBody = assemblyClassBody({
86    isSystem,
87    classEntity,
88    classBody,
89    className,
90    isExtend,
91    sourceFile,
92    mockApi,
93    isInnerMockFunction,
94    filename,
95    extraImport,
96    importDeclarations
97  });
98  return classBody;
99}
100
101/**
102 * generate some class
103 * @param porps
104 * @returns
105 */
106function assemblyClassBody(porps: AssemblyClassParams): string {
107  if (!porps.isSystem) {
108    porps.classBody += '{';
109    if (porps.classEntity.classConstructor.length > 1) {
110      porps.classBody += 'constructor(...arg) { ';
111    } else {
112      porps.classBody += 'constructor() { ';
113    }
114    if (porps.isExtend) {
115      porps.classBody += 'super();\n';
116    }
117    const warnCon = getWarnConsole(porps.className, 'constructor');
118    porps.classBody += porps.sourceFile.fileName.endsWith('PermissionRequestResult.d.ts') ? '' : warnCon;
119  }
120  if (porps.classEntity.classProperty.length > 0) {
121    porps.classEntity.classProperty.forEach(value => {
122      porps.classBody +=
123        generatePropertyDeclaration(
124          porps.className,
125          value,
126          porps.sourceFile,
127          porps.extraImport,
128          porps.importDeclarations
129        ) + '\n';
130    });
131  }
132
133  if (porps.classEntity.classMethod.size > 0) {
134    porps.classEntity.classMethod.forEach(value => {
135      porps.classBody += generateCommonMethod(porps.className, value, porps.sourceFile, porps.mockApi);
136    });
137  }
138
139  porps.classBody += '}\n};';
140  porps.classBody = assemblyGlobal(porps);
141
142  if (!porps.filename.startsWith('system_')) {
143    if (porps.classEntity.staticMethods.length > 0) {
144      let staticMethodBody = '';
145      porps.classEntity.staticMethods.forEach(value => {
146        staticMethodBody += generateStaticFunction(value, false, porps.sourceFile, porps.mockApi) + '\n';
147      });
148      porps.classBody += staticMethodBody;
149    }
150  }
151  if (porps.classEntity.exportModifiers.includes(SyntaxKind.DefaultKeyword)) {
152    porps.classBody += `\nexport default ${porps.className};`;
153  }
154  return porps.classBody;
155}
156
157/**
158 * generate some class
159 * @param porps
160 * @returns
161 */
162function assemblyGlobal(porps: AssemblyClassParams): string {
163  if (
164    (porps.classEntity.exportModifiers.includes(SyntaxKind.ExportKeyword) ||
165      porps.classEntity.exportModifiers.includes(SyntaxKind.DeclareKeyword)) &&
166    !porps.isInnerMockFunction
167  ) {
168    porps.classBody += `
169      if (!global.${porps.className}) {
170        global.${porps.className} = ${porps.className};\n
171      }
172    `;
173  }
174  return porps.classBody;
175}
176
177/**
178 * generate class
179 * @param rootName
180 * @param classEntity
181 * @returns
182 */
183function handleClassEntityHeritageClauses(
184  rootName: string,
185  classEntity: ClassEntity,
186  mockApi: string,
187  sourceFile: SourceFile
188): { isExtend: boolean; classBody: string } {
189  let isExtend = false;
190  let classBody = '';
191  if (classEntity.heritageClauses.length > 0) {
192    classEntity.heritageClauses.forEach(value => {
193      if (value.clauseToken === 'extends') {
194        isExtend = true;
195        classBody += `${value.clauseToken} `;
196        classBody = generateClassEntityHeritageClauses(classEntity, value, classBody, rootName, mockApi, sourceFile);
197      }
198    });
199  }
200  return {
201    isExtend,
202    classBody
203  };
204}
205
206/**
207 * generate classEntity heritageClauses
208 * @param classEntity
209 * @param value
210 * @param classBody
211 * @param rootName
212 * @param mockApi
213 * @param sourceFile
214 * @returns
215 */
216function generateClassEntityHeritageClauses(
217  classEntity: ClassEntity,
218  value: HeritageClauseEntity,
219  classBody: string,
220  rootName: string,
221  mockApi: string,
222  sourceFile: SourceFile
223): string {
224  value.types.forEach((val, index) => {
225    const extendClassName = val.trim().split('<')[0];
226    const moduleName = firstCharacterToUppercase(rootName);
227    if (val.startsWith('Array<')) {
228      val = 'Array';
229    } else {
230      if (classEntity.exportModifiers.includes(SyntaxKind.ExportKeyword) && rootName !== '') {
231        val = `mock${moduleName}().${val}`;
232      }
233    }
234    if (index !== value.types.length - 1) {
235      classBody += `${extendClassName},`;
236    } else if (val.includes('.')) {
237      const name = val.split('.')[0];
238      if (
239        mockApi.includes(`import { mock${firstCharacterToUppercase(name)} }`) &&
240        path.basename(sourceFile.fileName).startsWith('@ohos.')
241      ) {
242        classBody += val.replace(name, `mock${firstCharacterToUppercase(name)}()`);
243      } else {
244        classBody += `${extendClassName}`;
245      }
246    } else {
247      classBody += `${extendClassName}`;
248    }
249  });
250  return classBody;
251}
252
253/**
254 * add custome class
255 * @param heritageClausesData
256 * @param sourceFile
257 * @returns
258 */
259function addCustomeClass(
260  heritageClausesData: { isExtend: boolean; classBody: string },
261  sourceFile: SourceFile,
262  importDeclarations?: ImportElementEntity[]
263): string {
264  if (!heritageClausesData.isExtend) {
265    return '';
266  }
267  if (
268    !path.resolve(sourceFile.fileName).includes(path.join('@internal', 'component', 'ets')) &&
269    path.basename(sourceFile.fileName).startsWith('@ohos.')
270  ) {
271    return '';
272  }
273  let mockClassBody = '';
274  if (!heritageClausesData.classBody.startsWith('extends ')) {
275    return mockClassBody;
276  }
277  const classArr = heritageClausesData.classBody.split('extends');
278  const className = classArr[classArr.length - 1].trim();
279  if (className === 'extends') {
280    return mockClassBody;
281  }
282  const removeNoteRegx = /\/\*[\s\S]*?\*\//g;
283  const fileContent = sourceFile.getText().replace(removeNoteRegx, '');
284  let hasImportType = false;
285  if (importDeclarations) {
286    importDeclarations.forEach(element => {
287      if (element.importElements.includes(className)) {
288        hasImportType = true;
289      }
290    });
291  }
292  const regex = new RegExp(`\\sclass\\s*${className}\\s*(<|{|extends|implements)`);
293  const results = fileContent.match(regex);
294  if (!results && !hasImportType) {
295    mockClassBody = `export class ${className} {};\n`;
296  }
297  return mockClassBody;
298}
299