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 16const { readFile, etsComponentSet, collectAllApi, callMethod } = require('./util'); 17const { excel } = require('./collectApi'); 18const { collectBaseApi } = require('./format'); 19const path = require('path'); 20const fs = require('fs'); 21const ts = require('typescript'); 22 23let allCallApisInApp = []; 24 25function collectApis(url) { 26 const applicationUrl = path.resolve(__dirname, url); 27 const applicationFiles = []; 28 readFile(applicationUrl, applicationFiles); 29 if (applicationFiles.length === 0) { 30 console.error('ERROR:application directory is empty!'); 31 } else { 32 parseFileContent(applicationFiles, visitEachNode); 33 const noRepeatApis = deleteRepeatApis(allCallApisInApp); 34 excel(noRepeatApis); 35 } 36} 37 38function deleteRepeatApis(allApis) { 39 let allApisSet = new Set(); 40 let noRepeatApis = []; 41 allApis.forEach(api => { 42 allApisSet.add(JSON.stringify(api)); 43 }); 44 allApisSet.forEach(item => { 45 noRepeatApis.push(JSON.parse(item)); 46 }); 47 return noRepeatApis; 48} 49 50function parseFileContent(applicationFiles, callback) { 51 applicationFiles.forEach(url => { 52 if (/\.ets/.test(path.basename(url)) || /\.ts/.test(path.basename(url)) || 53 /\.js(?!on)/.test(path.basename(url))) { 54 const content = fs.readFileSync(url, 'utf-8'); 55 const fileName = path.basename(url).replace(/\.d.ts$|\.js/g, 'ts'); 56 ts.transpileModule(content, { 57 compilerOptions: { 58 'target': ts.ScriptTarget.ES2017, 59 }, 60 fileName: fileName, 61 transformers: { before: [callback(url)] }, 62 }); 63 } 64 }); 65} 66 67function visitEachNode(url) { 68 return (context) => { 69 return (sourcefile) => { 70 const statements = sourcefile.statements; 71 // 存放import的d.ts文件和类 72 let importFiles = []; 73 // 存放符合调用条件的API和组件 74 let apiList = []; 75 statements.forEach(item => { 76 if (ts.isImportDeclaration(item)) { 77 judgeImportFile(item, importFiles); 78 } else { 79 collectApplicationApi(item, sourcefile, url, apiList); 80 } 81 }); 82 apiList = addPackageName(apiList, importFiles); 83 handleInstantiatedCall(apiList); 84 allCallApisInApp = allCallApisInApp.concat(collectBaseApi(importFiles, apiList)); 85 return sourcefile; 86 }; 87 }; 88} 89 90function handleInstantiatedCall(apiList) { 91 apiList.forEach(instantiatedApi => { 92 apiList.forEach(api => { 93 if (api !== undefined && instantiatedApi !== undefined && instantiatedApi.instantiateObject === api.moduleName) { 94 // 将所有实例化调用方式的API备注统一改为‘实例化对象方式调用’,便于后续处理。 95 api.notes = '实例化对象方式调用'; 96 if (instantiatedApi.notes === callMethod.firstCallMethod) { 97 api.packageName = instantiatedApi.packageName; 98 } else if (instantiatedApi.notes === callMethod.secondCallMethod) { 99 api.packageName = instantiatedApi.packageName; 100 api.moduleName = instantiatedApi.apiName; 101 } else if (instantiatedApi.notes === callMethod.thirdCallMethod) { 102 api.packageName = instantiatedApi.packageName; 103 api.moduleName = instantiatedApi.moduleName; 104 } else if (instantiatedApi.notes === callMethod.fourthCallMethod) { 105 api.packageName = instantiatedApi.packageName; 106 api.moduleName = instantiatedApi.apiName; 107 108 } 109 } 110 }); 111 }); 112} 113 114// 收集import的文件名和类 115function judgeImportFile(node, importFiles) { 116 if (isImportFiles(node)) { 117 let importFileName = node.moduleSpecifier.text; 118 if (node.importClause && node.importClause.name !== undefined) { 119 importFiles.push({ 120 importFile: importFileName, 121 importClass: node.importClause.name.escapedText, 122 }); 123 } else if (node.importClause.namedBindings !== undefined && 124 ts.isNamedImports(node.importClause.namedBindings)) { 125 node.importClause.namedBindings.elements.forEach(element => { 126 importFiles.push({ 127 importFile: importFileName, 128 importClass: element.name.escapedText, 129 }); 130 }); 131 } 132 } 133} 134 135/** 136 * 收集所有的API,包含组件和API 137 * @param {ts.node} node 138 * @param {ts.sourcefile} sourcefile 139 * @param {string} url 140 * @param {Array} apiList [{callLocation:'', moduleName:'', apiName: '', packageName: '', 141 * instantiateObject:'',interfaceName: '',value:'', type:'',notes:''}] 142 */ 143function collectApplicationApi(node, sourcefile, url, apiList) { 144 if (ts.isPropertyAccessExpression(node) && node.expression && ts.isIdentifier(node.name)) { 145 collectCommonCallApis(node, sourcefile, url, apiList); 146 } else if (ts.isQualifiedName(node) && ts.isTypeReferenceNode(node.parent)) { 147 if (node.parent.parent.name && ts.isIdentifier(node.parent.parent.name)) { 148 const note = callMethod.secondCallMethod; 149 const type = 'API'; 150 const instantiateObject = node.parent.parent.name.escapedText; 151 const moduleName = node.left.escapedText; 152 const apiName = node.right.escapedText; 153 apiList.push(collectAllApi(url, sourcefile, moduleName, apiName, instantiateObject, '', '', type, note, node)); 154 } else { 155 const type = 'API'; 156 const instantiateObject = ''; 157 const moduleName = node.left.escapedText; 158 const apiName = node.right.escapedText; 159 apiList.push(collectAllApi(url, sourcefile, moduleName, apiName, instantiateObject, '', '', type, '', node)); 160 } 161 162 } else if (ts.isNewExpression(node) && ts.isPropertyDeclaration(node.parent)) { 163 collectNewExpressionApi(node, url, sourcefile, apiList); 164 } else if (ts.isClassDeclaration(node) && node.heritageClauses && node.members) { 165 collectLifeCycleApi(node, url, sourcefile, apiList); 166 } else if (isEtsComponentNode(node)) { 167 const type = 'ArkUI'; 168 collectComponentApi(node, apiList, type, url, sourcefile); 169 if (node.arguments && ts.isIdentifier(node.expression)) { 170 collectComponentApis(sourcefile, url, type, node, apiList); 171 } 172 } 173 node.getChildren().forEach(item => collectApplicationApi(item, sourcefile, url, apiList)); 174} 175 176function collectNewExpressionApi(node, url, sourcefile, apiList) { 177 if (etsComponentSet.has(node.expression.escapedText)) { 178 const type = 'ArkUI'; 179 collectComponentApis(sourcefile, url, type, node, apiList); 180 } else if (ts.isPropertyAccessExpression(node.expression)) { 181 const moduleName = node.expression.expression.escapedText; 182 const apiName = node.expression.name.escapedText; 183 const instantiateObject = node.parent.name.escapedText; 184 const note = callMethod.fourthCallMethod; 185 const type = 'API'; 186 apiList.push(collectAllApi(url, sourcefile, moduleName, apiName, instantiateObject, '', '', type, note, node)); 187 } else { 188 const note = callMethod.thirdCallMethod; 189 const type = 'API'; 190 const instantiateObject = node.parent.name.escapedText; 191 const moduleName = node.expression.escapedText; 192 apiList.push(collectAllApi(url, sourcefile, moduleName, '', instantiateObject, '', '', type, note, node)); 193 } 194} 195 196function isEtsComponentNode(node) { 197 return ts.isEtsComponentExpression(node) || (ts.isCallExpression(node) && node.expression && 198 ts.isIdentifier(node.expression) && etsComponentSet.has(node.expression.escapedText.toString())); 199} 200 201// 收集生命周期类型的API。 202function collectLifeCycleApi(node, url, sourcefile, apiList) { 203 const classNode = node.heritageClauses[0].types[0].expression; 204 const note = ''; 205 const type = 'API'; 206 207 if (ts.isIdentifier(classNode)) { 208 const moduleName = classNode.escapedText; 209 getLifeCycleApiWithoutValue(node.members, moduleName, type, note, node, apiList, url, sourcefile); 210 } else if (ts.isPropertyAccessExpression(classNode)) { 211 const moduleName = classNode.expression.escapedText; 212 const apiName = classNode.name.escapedText; 213 getValuableLifeCycleApi(node.members, moduleName, apiName, type, note, node, apiList, url, sourcefile); 214 } 215} 216 217function getLifeCycleApiWithoutValue(members, moduleName, type, note, node, apiList, url, sourcefile) { 218 members.forEach(member => { 219 if (ts.isConstructorDeclaration(member)) { 220 const apiName = 'constructor'; 221 apiList.push(collectAllApi(url, sourcefile, moduleName, apiName, '', '', '', type, note, node)); 222 } else { 223 const apiName = member.name ? member.name.escapedText : ''; 224 apiList.push(collectAllApi(url, sourcefile, moduleName, apiName, '', '', '', type, note, node)); 225 } 226 }); 227} 228 229function getValuableLifeCycleApi(members, moduleName, apiName, type, note, node, apiList, url, sourcefile) { 230 let value = ''; 231 232 members.forEach(member => { 233 if (ts.isConstructorDeclaration(member)) { 234 value = 'constructor'; 235 apiList.push(collectAllApi(url, sourcefile, moduleName, apiName, '', '', value, type, note, node)); 236 } else { 237 value = member.name ? member.name.escapedText : ''; 238 apiList.push(collectAllApi(url, sourcefile, moduleName, apiName, '', '', value, type, note, node)); 239 } 240 }); 241} 242 243function collectComponentApis(sourcefile, url, type, componentNode, apiList) { 244 let componentName = componentNode.expression.escapedText; 245 const notes = '比较API'; 246 247 if (componentNode.arguments) { 248 componentNode.arguments.forEach(argument => { 249 if (ts.isObjectLiteralExpression(argument)) { 250 let componentApiArr = collectNestedComponentApi(argument); 251 componentApiArr.forEach(componentApi => { 252 apiList.push(collectAllApi(url, sourcefile, componentName, componentApi, '', 253 '', '', type, notes, componentNode)); 254 }); 255 } 256 }); 257 } 258} 259 260// 收集组件中写在{}里调用的API,可能会有嵌套调用的情况 261function collectNestedComponentApi(node) { 262 let resultArr = []; 263 264 if (ts.isObjectLiteralExpression(node)) { 265 node.properties.forEach(property => { 266 if (ts.isPropertyAssignment(property) && property.name && ts.isIdentifier(property.name)) { 267 resultArr.push(property.name.escapedText.toString()); 268 if (property.initializer && ts.isObjectLiteralExpression(property.initializer)) { 269 resultArr = resultArr.concat(collectNestedComponentApi(property.initializer)); 270 } 271 } 272 }); 273 } 274 return resultArr; 275} 276 277function collectComponentApi(node, apiList, type, url, sourcefile) { 278 const notes = ''; 279 let etsComponentBlockPos = new Set([]); 280 const componentName = node.expression.escapedText ? node.expression.escapedText.toString() : 281 node.expression.expression.escapedText.toString(); 282 283 if (ts.isEtsComponentExpression(node) && ts.isBlock(node.parent.parent) && 284 !etsComponentBlockPos.has(node.parent.parent.pos)) { 285 etsComponentBlockPos.add(node.parent.parent); 286 const blockNode = node.parent.parent; 287 const statements = blockNode.statements; 288 statements.forEach((stat, index) => { 289 if (stat.expression && ts.isEtsComponentExpression(stat.expression) && 290 (componentName === stat.expression.escapedText || componentName === stat.expression.expression.escapedText)) { 291 getCommonCallComponentApi(statements, url, sourcefile, componentName, type, notes, apiList, index, stat); 292 } 293 }); 294 } else if (ts.isCallExpression(node)) { 295 let temp = node.parent; 296 while (!ts.isExpressionStatement(temp) && !(ts.isCallExpression(temp) && ts.isCallExpression(temp.parent))) { 297 collectExpressionStatementApis(temp, url, sourcefile, componentName, type, notes, node, apiList); 298 temp = temp.parent; 299 } 300 } 301} 302 303function collectExpressionStatementApis(temp, url, sourcefile, componentName, type, notes, node, apiList) { 304 if (ts.isPropertyAccessExpression(temp)) { 305 if (ts.isPropertyAccessExpression(temp)) { 306 apiName = temp.name.escapedText.toString(); 307 apiList.push(collectAllApi(url, sourcefile, componentName, apiName, '', '', '', type, notes, node)); 308 } else if (ts.isIdentifier(temp)) { 309 apiName = temp.escapedText.toString(); 310 apiList.push(collectAllApi(url, sourcefile, componentName, apiName, '', '', '', type, notes, node)); 311 } 312 } 313} 314 315function getCommonCallComponentApi(statements, url, sourcefile, componentName, type, notes, apiList, index, stat) { 316 if (index + 1 < statements.length && ts.isExpressionStatement(statements[index + 1]) && 317 statements[index + 1].expression && ts.isCallExpression(statements[index + 1].expression)) { 318 let temp = statements[index + 1].expression.expression; 319 while (temp) { 320 if (ts.isPropertyAccessExpression(temp)) { 321 apiName = temp.name.escapedText.toString(); 322 apiList.push(collectAllApi(url, sourcefile, componentName, apiName, '', '', '', type, notes, temp)); 323 } else if (ts.isIdentifier(temp)) { 324 apiName = temp.escapedText.toString(); 325 apiList.push(collectAllApi(url, sourcefile, componentName, apiName, '', '', '', type, notes, temp)); 326 } 327 temp = temp.expression; 328 } 329 } 330} 331 332// 收集常见调用方式的API 333function collectCommonCallApis(node, sourcefile, url, apiList) { 334 let type = 'API'; 335 let moduleName = ''; 336 let apiName = ''; 337 338 if (ts.isCallExpression(node.expression) && ts.isPropertyAccessExpression(node.expression.expression) && 339 node.expression.expression.expression.escapedText) { 340 moduleName = node.expression.expression.expression.escapedText; 341 apiName = node.name.escapedText.toString; 342 apiList.push(collectAllApi(url, sourcefile, moduleName, apiName, '', '', '', type, '', node)); 343 } else if (ts.isPropertyAccessExpression(node.expression) && node.expression.expression) { 344 if (ts.isIdentifier(node.expression.expression)) { 345 moduleName = node.expression.expression.escapedText; 346 apiName = node.expression.name.escapedText; 347 const value = node.name.escapedText; 348 apiList.push(collectAllApi(url, sourcefile, moduleName, apiName, '', '', value, type, '', node)); 349 } else { 350 moduleName = node.expression.name.escapedText; 351 apiName = node.name.escapedText; 352 apiList.push(collectAllApi(url, sourcefile, moduleName, apiName, '', '', '', type, '', node)); 353 } 354 } else if (ts.isIdentifier(node.expression) && ts.isCallExpression(node.parent)) { 355 apiList.push(collectOnOffApi(node, url, type, sourcefile)); 356 } else if (ts.isIdentifier(node.expression) && ts.isIdentifier(node.name)) { 357 moduleName = node.expression.escapedText; 358 apiName = node.name.escapedText; 359 apiList.push(collectAllApi(url, sourcefile, moduleName, apiName, '', '', '', type, '', node)); 360 } 361} 362 363// 收集到的API是没有d.ts文件名的,通过这个函数添加上 364function addPackageName(apiList, importFiles) { 365 importFiles.forEach(importData => { 366 apiList.forEach(api => { 367 if (api !== undefined && importData.importClass.match(new RegExp(api.moduleName, 'i'))) { 368 api.packageName = importData.importFile; 369 } 370 }); 371 }); 372 return apiList; 373} 374 375// API名字为on/off的单独处理,拼接上type类型。 376function collectOnOffApi(node, url, type, sourcefile) { 377 const moduleName = node.expression.escapedText; 378 let instantiateObject = ''; 379 let apiName = ''; 380 let note = ''; 381 if (node.parent.arguments && node.name.escapedText.toString() === 'on' || 382 node.name.escapedText.toString() === 'off') { 383 node.parent.arguments.forEach(argument => { 384 if (ts.isStringLiteral(argument) || ts.isIdentifier(argument)) { 385 apiName = node.name.escapedText + '_' + argument.text; 386 } 387 }); 388 } else if (ts.isVariableDeclaration(node.parent.parent)) { 389 instantiateObject = node.parent.parent.name.escapedText; 390 apiName = node.name.escapedText; 391 note = callMethod.firstCallMethod; 392 } else { 393 apiName = node.name.escapedText; 394 } 395 if (apiName !== '') { 396 return collectAllApi(url, sourcefile, moduleName, apiName, instantiateObject, '', '', type, note, node); 397 } 398 return {}; 399} 400 401function isImportFiles(node) { 402 if (ts.isStringLiteral(node.moduleSpecifier) && ((node.moduleSpecifier.text).indexOf('@ohos.') !== -1 || 403 (node.moduleSpecifier.text).indexOf('@system.') !== -1) && node.importClause !== undefined) { 404 return true; 405 } 406 return false; 407} 408 409try { 410 collectApis('../application'); 411} catch (error) { 412 console.error('COLLECT IMPORT NAME ERROR: ', error); 413}