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