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 type { 17 ModifiersArray, 18 Node, 19 SourceFile 20} from 'typescript'; 21 22import { 23 createSourceFile, 24 forEachChild, 25 isBinaryExpression, 26 isClassDeclaration, 27 isClassExpression, 28 isStructDeclaration, 29 isExpressionStatement, 30 isEnumDeclaration, 31 isExportAssignment, 32 isExportDeclaration, 33 isExportSpecifier, 34 isIdentifier, 35 isInterfaceDeclaration, 36 isObjectLiteralExpression, 37 isTypeAliasDeclaration, 38 isVariableDeclaration, 39 isVariableStatement, 40 isElementAccessExpression, 41 isPropertyAccessExpression, 42 isStringLiteral, 43 ScriptTarget, 44 SyntaxKind, 45} from 'typescript'; 46 47import fs from 'fs'; 48import path from 'path'; 49import { 50 getClassProperties, 51 getElementAccessExpressionProperties, 52 getEnumProperties, getInterfaceProperties, 53 getObjectProperties, 54 getTypeAliasProperties, 55} from '../utils/OhsUtil'; 56import {scanProjectConfig} from './ApiReader'; 57import {stringPropsSet} from '../utils/OhsUtil'; 58 59export namespace ApiExtractor { 60 interface KeywordInfo { 61 hasExport: boolean, 62 hasDeclare: boolean 63 } 64 65 export enum ApiType { 66 API = 1, 67 COMPONENT = 2, 68 PROJECT_DEPENDS = 3, 69 PROJECT = 4 70 } 71 72 let mCurrentExportNameSet: Set<string> = new Set<string>(); 73 export let mPropertySet: Set<string> = new Set<string>(); 74 75 /** 76 * filter classes or interfaces with export, default, etc 77 */ 78 const getKeyword = function (modifiers: ModifiersArray): KeywordInfo { 79 if (modifiers === undefined) { 80 return {hasExport: false, hasDeclare: false}; 81 } 82 83 let hasExport: boolean = false; 84 let hasDeclare: boolean = false; 85 86 for (const modifier of modifiers) { 87 if (modifier.kind === SyntaxKind.ExportKeyword) { 88 hasExport = true; 89 } 90 91 if (modifier.kind === SyntaxKind.DeclareKeyword) { 92 hasDeclare = true; 93 } 94 } 95 96 return {hasExport: hasExport, hasDeclare: hasDeclare}; 97 }; 98 99 /** 100 * get export name list 101 * @param astNode 102 */ 103 const visitExport = function (astNode): void { 104 if (isExportAssignment(astNode)) { 105 if (!mCurrentExportNameSet.has(astNode.expression.getText())) { 106 mCurrentExportNameSet.add(astNode.expression.getText()); 107 mPropertySet.add(astNode.expression.getText()); 108 } 109 110 return; 111 } 112 113 let {hasExport, hasDeclare} = getKeyword(astNode.modifiers); 114 if (!hasExport) { 115 addCommonJsExports(astNode); 116 return; 117 } 118 119 if (astNode.name) { 120 if (!mCurrentExportNameSet.has(astNode.name.getText())) { 121 mCurrentExportNameSet.add(astNode.name.getText()); 122 mPropertySet.add(astNode.name.getText()); 123 } 124 125 return; 126 } 127 128 if (hasDeclare && astNode.declarationList) { 129 astNode.declarationList.declarations.forEach((declaration) => { 130 const declarationName = declaration.name.getText(); 131 if (!mCurrentExportNameSet.has(declarationName)) { 132 mCurrentExportNameSet.add(declarationName); 133 mPropertySet.add(declarationName); 134 } 135 }); 136 } 137 }; 138 139 const checkPropertyNeedVisit = function (astNode): boolean { 140 if (astNode.name && !mCurrentExportNameSet.has(astNode.name.getText())) { 141 return false; 142 } 143 144 if (astNode.name === undefined) { 145 let {hasDeclare} = getKeyword(astNode.modifiers); 146 if (hasDeclare && astNode.declarationList && 147 !mCurrentExportNameSet.has(astNode.declarationList.declarations[0].name.getText())) { 148 return false; 149 } 150 } 151 152 return true; 153 }; 154 155 /** 156 * used only in oh sdk api extract or api of xxx.d.ts declaration file 157 * @param astNode 158 */ 159 const visitChildNode = function (astNode): void { 160 if (!astNode) { 161 return; 162 } 163 164 if (astNode.name !== undefined && !mPropertySet.has(astNode.name.getText())) { 165 if (isStringLiteral(astNode.name)) { 166 mPropertySet.add(astNode.name.text); 167 } else { 168 mPropertySet.add(astNode.name.getText()); 169 } 170 } 171 172 astNode.forEachChild((childNode) => { 173 visitChildNode(childNode); 174 }); 175 }; 176 177 /** 178 * visit ast of a file and collect api list 179 * used only in oh sdk api extract 180 * @param astNode node of ast 181 */ 182 const visitPropertyAndName = function (astNode): void { 183 if (!checkPropertyNeedVisit(astNode)) { 184 return; 185 } 186 187 visitChildNode(astNode); 188 }; 189 190 /** 191 * commonjs exports extract 192 * examples: 193 * - exports.A = 1; 194 * - exports.B = hello; // hello can be variable or class ... 195 * - exports.C = {}; 196 * - exports.D = class {}; 197 * - exports.E = function () {} 198 * - class F {} 199 * - exports.F = F; 200 * - module.exports = {G: {}} 201 * - ... 202 */ 203 const addCommonJsExports = function (astNode): void { 204 if (!isExpressionStatement(astNode) || !astNode.expression) { 205 return; 206 } 207 208 const expression = astNode.expression; 209 if (!isBinaryExpression(expression)) { 210 return; 211 } 212 213 const left = expression.left; 214 if (!isElementAccessExpression(left) && !isPropertyAccessExpression(left)) { 215 return; 216 } 217 218 if ((left.expression.getText() !== 'exports' && !isModuleExports(left)) || 219 expression.operatorToken.kind !== SyntaxKind.EqualsToken) { 220 return; 221 } 222 223 if (isElementAccessExpression(left)) { 224 if (isStringLiteral(left.argumentExpression)) { 225 mPropertySet.add(left.argumentExpression.text); 226 } 227 } 228 229 if (isPropertyAccessExpression(left)) { 230 if (isIdentifier(left.name)) { 231 mPropertySet.add(left.name.getText()); 232 } 233 } 234 235 if (isIdentifier(expression.right)) { 236 mCurrentExportNameSet.add(expression.right.getText()); 237 return; 238 } 239 240 if (isClassDeclaration(expression.right) || isClassExpression(expression.right)) { 241 getClassProperties(expression.right, mPropertySet); 242 return; 243 } 244 245 if (isObjectLiteralExpression(expression.right)) { 246 getObjectProperties(expression.right, mPropertySet); 247 } 248 249 return; 250 }; 251 252 // module.exports = { p1: 1 } 253 function isModuleExports(astNode: Node): boolean { 254 if (isPropertyAccessExpression(astNode)) { 255 if (isIdentifier(astNode.expression) && astNode.expression.escapedText.toString() === 'module' && 256 isIdentifier(astNode.name) && astNode.name.escapedText.toString() === 'exports') { 257 return true; 258 } 259 } 260 return false; 261 } 262 263 /** 264 * extract project export name 265 * - export {xxx, xxx}; 266 * - export {xxx as xx, xxx as xx}; 267 * - export default function/class/...{}; 268 * - export class xxx{} 269 * - ... 270 * @param astNode 271 */ 272 const visitProjectExport = function (astNode): void { 273 if (isExportAssignment(astNode)) { 274 // let xxx; export default xxx = a; 275 if (isBinaryExpression(astNode.expression)) { 276 if (isObjectLiteralExpression(astNode.expression.right)) { 277 getObjectProperties(astNode.expression.right, mPropertySet); 278 return; 279 } 280 281 if (isClassExpression(astNode.expression.right)) { 282 getClassProperties(astNode.expression.right, mPropertySet); 283 } 284 285 return; 286 } 287 288 // export = xxx; The xxx here can't be obfuscated 289 // export default yyy; The yyy here can be obfuscated 290 if (isIdentifier(astNode.expression)) { 291 if (!mCurrentExportNameSet.has(astNode.expression.getText())) { 292 mCurrentExportNameSet.add(astNode.expression.getText()); 293 mPropertySet.add(astNode.expression.getText()); 294 } 295 return; 296 } 297 298 if (isObjectLiteralExpression(astNode.expression)) { 299 getObjectProperties(astNode.expression, mPropertySet); 300 } 301 302 return; 303 } 304 305 if (isExportDeclaration(astNode)) { 306 if (astNode.exportClause) { 307 if (astNode.exportClause.kind === SyntaxKind.NamedExports) { 308 astNode.exportClause.forEachChild((child) => { 309 if (!isExportSpecifier(child)) { 310 return; 311 } 312 313 if (child.propertyName) { 314 mCurrentExportNameSet.add(child.propertyName.getText()); 315 } 316 317 let exportName = child.name.getText(); 318 mPropertySet.add(exportName); 319 mCurrentExportNameSet.add(exportName); 320 }); 321 } 322 323 if (astNode.exportClause.kind === SyntaxKind.NamespaceExport) { 324 mPropertySet.add(astNode.exportClause.name.getText()); 325 return; 326 } 327 } 328 return; 329 } 330 331 let {hasExport} = getKeyword(astNode.modifiers); 332 if (!hasExport) { 333 addCommonJsExports(astNode); 334 forEachChild(astNode, visitProjectExport); 335 return; 336 } 337 338 if (astNode.name) { 339 if (!mCurrentExportNameSet.has(astNode.name.getText())) { 340 mCurrentExportNameSet.add(astNode.name.getText()); 341 mPropertySet.add(astNode.name.getText()); 342 } 343 344 forEachChild(astNode, visitProjectExport); 345 return; 346 } 347 348 if (isClassDeclaration(astNode)) { 349 getClassProperties(astNode, mPropertySet); 350 return; 351 } 352 353 if (isVariableStatement(astNode)) { 354 astNode.declarationList.forEachChild((child) => { 355 if (isVariableDeclaration(child) && !mCurrentExportNameSet.has(child.name.getText())) { 356 mCurrentExportNameSet.add(child.name.getText()); 357 mPropertySet.add(child.name.getText()); 358 } 359 }); 360 361 return; 362 } 363 364 forEachChild(astNode, visitProjectExport); 365 }; 366 367 /** 368 * extract the class, enum, and object properties of the export in the project before obfuscation 369 * class A{}; 370 * export = A; need to be considered 371 * export = namespace; 372 * This statement also needs to determine whether there is an export in the namespace, and namespaces are also allowed in the namespace 373 * @param astNode 374 */ 375 const visitProjectNode = function (astNode): void { 376 const currentPropsSet: Set<string> = new Set(); 377 let nodeName: string | undefined = astNode.name?.text; 378 if ((isClassDeclaration(astNode) || isStructDeclaration(astNode))) { 379 getClassProperties(astNode, currentPropsSet); 380 } else if (isEnumDeclaration(astNode)) { // collect export enum structure properties 381 getEnumProperties(astNode, currentPropsSet); 382 } else if (isVariableDeclaration(astNode)) { 383 if (astNode.initializer) { 384 if (isObjectLiteralExpression(astNode.initializer)) { 385 getObjectProperties(astNode.initializer, currentPropsSet); 386 } else if (isClassExpression(astNode.initializer)) { 387 getClassProperties(astNode.initializer, currentPropsSet); 388 } 389 } 390 nodeName = astNode.name?.getText(); 391 } else if (isInterfaceDeclaration(astNode)) { 392 getInterfaceProperties(astNode, currentPropsSet); 393 } else if (isTypeAliasDeclaration(astNode)) { 394 getTypeAliasProperties(astNode, currentPropsSet); 395 } else if (isElementAccessExpression(astNode)) { 396 getElementAccessExpressionProperties(astNode, currentPropsSet); 397 } else if (isObjectLiteralExpression(astNode)) { 398 getObjectProperties(astNode, currentPropsSet); 399 } else if (isClassExpression(astNode)) { 400 getClassProperties(astNode, currentPropsSet); 401 } 402 403 if (nodeName && mCurrentExportNameSet.has(nodeName)) { 404 addElement(currentPropsSet); 405 } 406 407 forEachChild(astNode, visitProjectNode); 408 }; 409 410 411 function addElement(currentPropsSet: Set<string>): void { 412 currentPropsSet.forEach((element: string) => { 413 mPropertySet.add(element); 414 }); 415 currentPropsSet.clear(); 416 } 417 /** 418 * parse file to api list and save to json object 419 * @param fileName file name of api file 420 * @param apiType 421 * @private 422 */ 423 const parseFile = function (fileName: string, apiType: ApiType): void { 424 const sourceFile: SourceFile = createSourceFile(fileName, fs.readFileSync(fileName).toString(), ScriptTarget.ES2015, true); 425 426 // get export name list 427 switch (apiType) { 428 case ApiType.COMPONENT: 429 forEachChild(sourceFile, visitChildNode); 430 break; 431 case ApiType.API: 432 mCurrentExportNameSet.clear(); 433 forEachChild(sourceFile, visitExport); 434 435 forEachChild(sourceFile, visitPropertyAndName); 436 mCurrentExportNameSet.clear(); 437 break; 438 case ApiType.PROJECT_DEPENDS: 439 case ApiType.PROJECT: 440 if (fileName.endsWith('.d.ts') || fileName.endsWith('.d.ets')) { 441 forEachChild(sourceFile, visitChildNode); 442 break; 443 } 444 445 mCurrentExportNameSet.clear(); 446 forEachChild(sourceFile, visitProjectExport); 447 forEachChild(sourceFile, visitProjectNode); 448 if (scanProjectConfig.mKeepStringProperty) { 449 stringPropsSet.forEach((element) => { 450 mPropertySet.add(element); 451 }); 452 stringPropsSet.clear(); 453 } 454 mCurrentExportNameSet.clear(); 455 break; 456 default: 457 break; 458 } 459 }; 460 461 const projectExtensions: string[] = ['.ets', '.ts', '.js']; 462 const projectDependencyExtensions: string[] = ['.d.ets', '.d.ts', '.ets', '.ts', '.js']; 463 /** 464 * traverse files of api directory 465 * @param apiPath api directory path 466 * @param apiType 467 * @private 468 */ 469 export const traverseApiFiles = function (apiPath: string, apiType: ApiType): void { 470 let fileNames: string[] = []; 471 if (fs.statSync(apiPath).isDirectory()) { 472 fileNames = fs.readdirSync(apiPath); 473 for (let fileName of fileNames) { 474 let filePath: string = path.join(apiPath, fileName); 475 if (fs.statSync(filePath).isDirectory()) { 476 traverseApiFiles(filePath, apiType); 477 continue; 478 } 479 const suffix: string = path.extname(filePath); 480 if ((apiType !== ApiType.PROJECT) && !projectDependencyExtensions.includes(suffix)) { 481 continue; 482 } 483 484 if (apiType === ApiType.PROJECT && !projectExtensions.includes(suffix)) { 485 continue; 486 } 487 parseFile(filePath, apiType); 488 } 489 } else { 490 parseFile(apiPath, apiType); 491 } 492 }; 493 494 /** 495 * desc: parse openHarmony sdk to get api list 496 * @param version version of api, e.g. version 5.0.1.0 for api 9 497 * @param sdkPath sdk real path of openHarmony 498 * @param isEts true for ets, false for js 499 * @param outputDir: sdk api output directory 500 */ 501 export function parseOhSdk(sdkPath: string, version: string, isEts: boolean, outputDir: string): void { 502 mPropertySet.clear(); 503 504 // visit api directory 505 const apiPath: string = path.join(sdkPath, (isEts ? 'ets' : 'js'), version, 'api'); 506 traverseApiFiles(apiPath, ApiType.API); 507 508 // visit component directory if ets 509 if (isEts) { 510 const componentPath: string = path.join(sdkPath, 'ets', version, 'component'); 511 traverseApiFiles(componentPath, ApiType.COMPONENT); 512 } 513 514 // visit the UI conversion API 515 const uiConversionPath: string = path.join(sdkPath, (isEts ? 'ets' : 'js'), version, 516 'build-tools', 'ets-loader', 'lib', 'pre_define.js'); 517 extractStringsFromFile(uiConversionPath); 518 519 const reservedProperties: string[] = [...mPropertySet.values()]; 520 mPropertySet.clear(); 521 522 writeToFile(reservedProperties, path.join(outputDir, 'propertiesReserved.json')); 523 } 524 525 export function extractStringsFromFile(filePath: string): void { 526 let collections: string[] = []; 527 const fileContent = fs.readFileSync(filePath, 'utf-8'); 528 const regex = /"([^"]*)"/g; 529 const matches = fileContent.match(regex); 530 531 if (matches) { 532 collections = matches.map(match => match.slice(1, -1)); 533 } 534 535 collections.forEach(name => mPropertySet.add(name)); 536 } 537 538 /** 539 * parse common project or file to extract exported api list 540 * @return reserved api names 541 */ 542 export function parseCommonProject(projectPath): string[] { 543 mPropertySet.clear(); 544 545 if (fs.lstatSync(projectPath).isFile()) { 546 if (projectPath.endsWith('.ets') || projectPath.endsWith('.ts') || projectPath.endsWith('.js')) { 547 parseFile(projectPath, ApiType.PROJECT); 548 } 549 } else { 550 traverseApiFiles(projectPath, ApiType.PROJECT); 551 } 552 553 const reservedProperties: string[] = [...mPropertySet]; 554 mPropertySet.clear(); 555 556 return reservedProperties; 557 } 558 559 /** 560 * parse api of third party libs like libs in node_modules 561 * @param libPath 562 */ 563 export function parseThirdPartyLibs(libPath): string[] { 564 mPropertySet.clear(); 565 566 if (fs.lstatSync(libPath).isFile()) { 567 if (libPath.endsWith('.ets') || libPath.endsWith('.ts') || libPath.endsWith('.js')) { 568 parseFile(libPath, ApiType.PROJECT_DEPENDS); 569 } 570 } else { 571 const filesAndfolders = fs.readdirSync(libPath); 572 for (let subPath of filesAndfolders) { 573 traverseApiFiles(path.join(libPath, subPath), ApiType.PROJECT_DEPENDS); 574 } 575 } 576 577 const reservedProperties: string[] = [...mPropertySet]; 578 mPropertySet.clear(); 579 580 return reservedProperties; 581 } 582 583 /** 584 * save api json object to file 585 * @private 586 */ 587 export function writeToFile(reservedProperties: string[], outputPath: string): void { 588 let str: string = JSON.stringify(reservedProperties, null, '\t'); 589 fs.writeFileSync(outputPath, str); 590 } 591} 592