• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 fs from 'fs';
17import path from 'path';
18import { ScriptTarget, SyntaxKind, createSourceFile } from 'typescript';
19import type { SourceFile } from 'typescript';
20import { collectAllLegalImports, dtsFileList, firstCharacterToUppercase, getAllFileNameList, getApiInputPath } from '../common/commonUtils';
21import type { ImportElementEntity } from '../declaration-node/importAndExportDeclaration';
22import { getDefaultExportClassDeclaration } from '../declaration-node/sourceFileElementsAssemply';
23import type { SourceFileEntity } from '../declaration-node/sourceFileElementsAssemply';
24import { generateClassDeclaration } from './generateClassDeclaration';
25import { generateEnumDeclaration } from './generateEnumDeclaration';
26import { addToIndexArray } from './generateIndex';
27import { generateInterfaceDeclaration } from './generateInterfaceDeclaration';
28import { generateModuleDeclaration } from './generateModuleDeclaration';
29import { generateStaticFunction } from './generateStaticFunction';
30import { addToSystemIndexArray } from './generateSystemIndex';
31import { generateTypeAliasDeclaration } from './generateTypeAlias';
32import { generateExportFunction } from './generateExportFunction';
33
34/**
35 * generate mock file string
36 * @param rootName
37 * @param sourceFileEntity
38 * @param sourceFile
39 * @param fileName
40 * @returns
41 */
42export function generateSourceFileElements(rootName: string, sourceFileEntity: SourceFileEntity, sourceFile: SourceFile, fileName: string): string {
43
44  let mockApi = '';
45  const mockFunctionElements: Array<MockFunctionElementEntity> = [];
46  const dependsSourceFileList = collectReferenceFiles(sourceFile);
47  const heritageClausesArray = getCurrentApiHeritageArray(sourceFileEntity, sourceFile);
48  const extraImport = [];
49
50  if (sourceFileEntity.importDeclarations.length > 0) {
51    sourceFileEntity.importDeclarations.forEach(value => {
52      mockApi += generateImportDeclaration(value, fileName, heritageClausesArray, sourceFile.fileName, dependsSourceFileList);
53    });
54  }
55
56  if (sourceFileEntity.moduleDeclarations.length > 0) {
57    sourceFileEntity.moduleDeclarations.forEach(value => {
58      mockApi += generateModuleDeclaration('', value, sourceFile, fileName, mockApi, extraImport) + '\n';
59    });
60  }
61
62  if (sourceFileEntity.classDeclarations.length > 0) {
63    sourceFileEntity.classDeclarations.forEach(value => {
64      if (!fileName.startsWith('system_') && !value.exportModifiers.includes(SyntaxKind.DefaultKeyword)) {
65        mockApi += generateClassDeclaration('', value, false, '', fileName, sourceFile, false, mockApi) + '\n';
66        mockFunctionElements.push({ elementName: value.className, type: 'class' });
67      }
68    });
69  }
70
71  if (sourceFileEntity.interfaceDeclarations.length > 0) {
72    sourceFileEntity.interfaceDeclarations.forEach(value => {
73      mockApi += generateInterfaceDeclaration('', value, sourceFile, true, mockApi, sourceFileEntity.interfaceDeclarations,
74        sourceFileEntity.importDeclarations, extraImport) + '\n';
75      mockFunctionElements.push({ elementName: value.interfaceName, type: 'interface' });
76    });
77  }
78
79  if (sourceFileEntity.enumDeclarations.length > 0) {
80    sourceFileEntity.enumDeclarations.forEach(value => {
81      mockApi += generateEnumDeclaration('', value) + '\n';
82      mockFunctionElements.push({ elementName: value.enumName, type: 'enum' });
83    });
84  }
85
86  if (sourceFileEntity.typeAliasDeclarations.length > 0) {
87    sourceFileEntity.typeAliasDeclarations.forEach(value => {
88      mockApi += generateTypeAliasDeclaration(value, false, sourceFile, extraImport) + '\n';
89      mockFunctionElements.push({ elementName: value.typeAliasName, type: 'typeAlias' });
90    });
91  }
92
93  if (sourceFileEntity.functionDeclarations.length > 0) {
94    sourceFileEntity.functionDeclarations.forEach(value => {
95      mockApi += generateExportFunction(value, sourceFile, mockApi) + '\n';
96    });
97  }
98
99  if (sourceFileEntity.moduleDeclarations.length === 0 && (fileName.startsWith('ohos_') || fileName.startsWith('system_') || fileName.startsWith('webgl'))) {
100    const mockNameArr = fileName.split('_');
101    const mockName = mockNameArr[mockNameArr.length - 1];
102    const defaultExportClass = getDefaultExportClassDeclaration(sourceFile);
103    if (defaultExportClass.length > 0) {
104      defaultExportClass.forEach(value => {
105        mockApi += generateClassDeclaration(rootName, value, false, mockName, '', sourceFile, false, mockApi) + '\n';
106        mockFunctionElements.push({ elementName: value.className, type: 'class' });
107      });
108    }
109    mockApi += `export function mock${firstCharacterToUppercase(mockName)}() {\n`;
110    if (fileName.startsWith('system_')) {
111      addToSystemIndexArray({
112        filename: fileName,
113        mockFunctionName: `mock${firstCharacterToUppercase(mockName)}`
114      });
115      mockApi += `global.systemplugin.${mockName} = {`;
116      const defaultClass = getDefaultExportClassDeclaration(sourceFile);
117      let staticMethodBody = '';
118      if (defaultClass.length > 0) {
119        defaultClass.forEach(value => {
120          value.staticMethods.forEach(val => {
121            staticMethodBody += generateStaticFunction(val, true, sourceFile, mockApi);
122          });
123        });
124      }
125      mockApi += staticMethodBody;
126      mockApi += '}';
127    } else {
128      if (!fileName.startsWith('webgl')) {
129        addToIndexArray({ fileName: fileName, mockFunctionName: `mock${firstCharacterToUppercase(mockName)}` });
130      }
131    }
132    mockApi += `\nconst mockModule${firstCharacterToUppercase(mockName)} = {`;
133    mockFunctionElements.forEach(val => {
134      mockApi += `${val.elementName}: ${val.elementName},`;
135    });
136    mockApi += '}\n';
137    mockApi += `return mockModule${firstCharacterToUppercase(mockName)}.${firstCharacterToUppercase(mockName)}\n`;
138    mockApi += '}';
139  } else {
140    const defaultExportClass = getDefaultExportClassDeclaration(sourceFile);
141    if (defaultExportClass.length > 0) {
142      const mockNameArr = fileName.split('_');
143      const mockName = mockNameArr[mockNameArr.length - 1];
144      defaultExportClass.forEach(value => {
145        mockApi += generateClassDeclaration(rootName, value, false, mockName, '', sourceFile, false, mockApi) + '\n';
146      });
147    }
148  }
149  if (sourceFileEntity.exportDeclarations.length > 0) {
150    sourceFileEntity.exportDeclarations.forEach(value => {
151      if (value.includes('export type {')) {
152        return;
153      }
154      if (!value.includes('export {')) {
155        mockApi += `${value}\n`;
156      }
157    });
158  }
159  mockApi = extraImport.join('') + mockApi;
160  return mockApi;
161}
162
163/**
164 * generate import definition
165 * @param importEntity
166 * @param sourceFileName
167 * @param heritageClausesArray
168 * @param currentFilePath
169 * @param dependsSourceFileList
170 * @returns
171 */
172export function generateImportDeclaration(
173  importEntity: ImportElementEntity,
174  sourceFileName: string,
175  heritageClausesArray: string[],
176  currentFilePath: string,
177  dependsSourceFileList: SourceFile[]): string {
178  const importDeclaration = referenctImport2ModuleImport(importEntity, currentFilePath, dependsSourceFileList);
179  if (importDeclaration) {
180    return importDeclaration;
181  }
182
183  const importPathSplit = importEntity.importPath.split('/');
184
185  let importPath = importPathSplit.slice(0, -1).join('/') + '/';
186  importPath += getImportPathName(importPathSplit);
187
188  let importElements = generateImportElements(importEntity, heritageClausesArray);
189  if (importElements === '{ mockWantAgent }' && importPath.includes('ohos_app_ability_wantAgent')) {
190    importElements = '{ mockWantAgent as mockAbilityWantAgent }';
191  }
192  const testPath = importPath.replace(/"/g, '').replace(/'/g, '').split('/');
193  if (!getAllFileNameList().has(testPath[testPath.length - 1]) && testPath[testPath.length - 1] !== 'ohos_application_want') {
194    return '';
195  }
196
197  let tmpImportPath = importPath.replace(/'/g, '').replace(/"/g, '');
198  if (!tmpImportPath.startsWith('./') && !tmpImportPath.startsWith('../')) {
199    importPath = `'./${tmpImportPath}'`;
200  }
201  if (sourceFileName === 'tagSession' && tmpImportPath === './basic' || sourceFileName === 'notificationContent' &&
202  tmpImportPath === './ohos_multimedia_image') {
203    importPath = `'.${importPath.replace(/'/g, '')}'`;
204  }
205
206  if (sourceFileName === 'AbilityContext' && tmpImportPath === '../ohos_application_Ability' ||
207    sourceFileName === 'Context' && tmpImportPath === './ApplicationContext') {
208    return '';
209  }
210  collectAllLegalImports(importElements);
211  return `import ${importElements} from ${importPath}\n`;
212}
213
214/**
215 * adapter default export
216 * @param importName
217 * @returns
218 */
219function checIsDefaultExportClass(importName: string): boolean {
220  const defaultExportClass = ['Context', 'BaseContext', 'ExtensionContext', 'ApplicationContext',
221    'ExtensionAbility', 'Ability', 'UIExtensionAbility', 'UIExtensionContext'];
222  return defaultExportClass.includes(importName);
223}
224
225/**
226 * get heritage elements
227 * @param sourceFileEntity
228 * @param sourceFile
229 * @returns
230 */
231function getCurrentApiHeritageArray(sourceFileEntity: SourceFileEntity, sourceFile: SourceFile): string[] {
232  const heritageClausesArray = [];
233  const defaultClassArray = getDefaultExportClassDeclaration(sourceFile);
234  sourceFileEntity.classDeclarations.forEach(value => {
235    value.heritageClauses.forEach(val => {
236      val.types.forEach(v => {
237        heritageClausesArray.push(v);
238      });
239    });
240  });
241  defaultClassArray.forEach(value => {
242    value.heritageClauses.forEach(val => {
243      val.types.forEach(v => {
244        heritageClausesArray.push(v);
245      });
246    });
247  });
248  return heritageClausesArray;
249}
250
251function collectReferenceFiles(sourceFile: SourceFile): SourceFile[] {
252  const referenceElementTemplate = /\/\/\/\s*<reference\s+path="[^'"\[\]]+/g;
253  const referenceFiles: SourceFile[] = [];
254  const text = sourceFile.text;
255  const referenceElement = text.match(referenceElementTemplate);
256
257  referenceElement && referenceElement.forEach(element => {
258    const referenceRelatePath = element.split(/path=["']/g)[1];
259    const realReferenceFilePath = contentRelatePath2RealRelatePath(sourceFile.fileName, referenceRelatePath);
260    if (!realReferenceFilePath) {
261      return;
262    }
263
264    if (!fs.existsSync(realReferenceFilePath)) {
265      console.error(`Can not resolve file: ${realReferenceFilePath}`);
266      return;
267    }
268    const code = fs.readFileSync(realReferenceFilePath);
269    referenceFiles.push(createSourceFile(realReferenceFilePath, code.toString(), ScriptTarget.Latest));
270    !dtsFileList.includes(realReferenceFilePath) && dtsFileList.push(realReferenceFilePath);
271  });
272  return referenceFiles;
273}
274
275function contentRelatePath2RealRelatePath(currentFilePath: string, contentReferenceRelatePath: string): string {
276  const conmponentSourceFileTemplate = /component\/[^'"\/]+\.d\.ts/;
277  const currentFolderSourceFileTemplate = /\.\/[^\/]+\.d\.ts/;
278  const baseFileNameTemplate = /[^\/]+\.d\.ts/;
279
280  let realReferenceFilePath: string;
281  if (conmponentSourceFileTemplate.test(contentReferenceRelatePath)) {
282    const newRelateReferencePath = contentReferenceRelatePath.match(conmponentSourceFileTemplate)[0];
283    const referenceFileName = path.basename(newRelateReferencePath);
284    realReferenceFilePath = path.join(getApiInputPath(), '@internal', 'component', 'ets', referenceFileName);
285  } else if (currentFolderSourceFileTemplate.test(contentReferenceRelatePath)) {
286    const referenceFileName = path.basename(contentReferenceRelatePath);
287    realReferenceFilePath = currentFilePath.replace(baseFileNameTemplate, referenceFileName).replace(/\//g, path.sep);
288  } else {
289    console.error(`Can not find reference ${contentReferenceRelatePath} from ${currentFilePath}`);
290    return '';
291  }
292  return realReferenceFilePath;
293}
294
295export function referenctImport2ModuleImport(importEntity: ImportElementEntity, currentFilePath: string,
296  dependsSourceFileList: SourceFile[]): string {
297  if (dependsSourceFileList.length && !importEntity.importPath.includes('.')) {
298    for (let i = 0; i < dependsSourceFileList.length; i++) {
299      if (dependsSourceFileList[i].text.includes(`declare module ${importEntity.importPath.replace(/'/g, '"')}`)) {
300        let relatePath = path.relative(path.dirname(currentFilePath), dependsSourceFileList[i].fileName)
301          .replace(/\\/g, '/')
302          .replace(/.d.ts/g, '')
303          .replace(/.d.es/g, '');
304        relatePath = (relatePath.startsWith('@internal/component') ? './' : '') + relatePath;
305        return `import ${importEntity.importElements} from "${relatePath}"\n`;
306      }
307    }
308  }
309  return '';
310}
311
312function getImportPathName(importPathSplit: string[]): string {
313  let importPathName: string;
314  let fileName = importPathSplit[importPathSplit.length - 1];
315  if (fileName.endsWith('.d.ts') || fileName.endsWith('.d.ets')) {
316    fileName = fileName.split(/\.d\.e?ts/)[0];
317  }
318  if (fileName.includes('@')) {
319    importPathName = fileName.replace('@', '').replace(/\./g, '_');
320  } else {
321    importPathName = fileName.replace(/\./g, '_');
322  }
323  return importPathName;
324}
325
326function generateImportElements(importEntity: ImportElementEntity, heritageClausesArray: string[]): string {
327  let importElements = importEntity.importElements;
328  if (!importElements.includes('{') && !importElements.includes('* as') && !heritageClausesArray.includes(importElements) && importEntity.importPath.includes('@ohos')) {
329    const tmpArr = importEntity.importPath.split('.');
330    importElements = `{ mock${firstCharacterToUppercase(tmpArr[tmpArr.length - 1].replace('"', '').replace('\'', ''))} }`;
331  } else {
332    // adapt no rules .d.ts
333    if (importElements.trimRight().trimEnd() === 'AccessibilityExtensionContext, { AccessibilityElement }') {
334      importElements = '{ AccessibilityExtensionContext, AccessibilityElement }';
335    } else if (importElements.trimRight().trimEnd() === '{ image }') {
336      importElements = '{ mockImage as image }';
337    }
338  }
339  return importElements;
340}
341
342
343interface MockFunctionElementEntity {
344  elementName: string,
345  type: string
346}
347