• 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 type {
17  ElementAccessExpression,
18  EnumDeclaration,
19  ExportDeclaration,
20  ModifiersArray,
21  ModuleDeclaration,
22  Node,
23  ParameterDeclaration,
24  PropertyAccessExpression,
25  SourceFile
26} from 'typescript';
27
28import {
29  createSourceFile,
30  forEachChild,
31  isBinaryExpression,
32  isClassDeclaration,
33  isClassExpression,
34  isStructDeclaration,
35  isExpressionStatement,
36  isEnumDeclaration,
37  isExportAssignment,
38  isExportDeclaration,
39  isExportSpecifier,
40  isIdentifier,
41  isInterfaceDeclaration,
42  isObjectLiteralExpression,
43  isTypeAliasDeclaration,
44  isVariableDeclaration,
45  isVariableStatement,
46  isElementAccessExpression,
47  isPropertyAccessExpression,
48  isStringLiteral,
49  ScriptTarget,
50  SyntaxKind,
51  sys,
52  isConstructorDeclaration,
53  getModifiers,
54  isNamedExports,
55  isNamespaceExport,
56  isPropertyDeclaration,
57  isPropertySignature,
58  isMethodDeclaration,
59  isMethodSignature,
60  isObjectLiteralElementLike,
61  isModuleDeclaration,
62  isPropertyAssignment,
63  isModuleBlock,
64  isFunctionDeclaration,
65  isEnumMember
66} from 'typescript';
67
68import fs from 'fs';
69import path from 'path';
70import json5 from 'json5';
71
72import {
73  exportOriginalNameSet,
74  getClassProperties,
75  getElementAccessExpressionProperties,
76  getEnumProperties, getInterfaceProperties,
77  getObjectExportNames,
78  getObjectProperties,
79  getTypeAliasProperties,
80  isParameterPropertyModifier,
81} from '../utils/OhsUtil';
82import { scanProjectConfig } from './ApiReader';
83import { stringPropsSet, enumPropsSet } from '../utils/OhsUtil';
84import type { IOptions } from '../configs/IOptions';
85import { FileUtils } from '../utils/FileUtils';
86import { supportedParsingExtension } from './type';
87
88export namespace ApiExtractor {
89  interface KeywordInfo {
90    hasExport: boolean,
91    hasDeclare: boolean
92  }
93
94  export enum ApiType {
95    API = 1,
96    COMPONENT = 2,
97    PROJECT_DEPENDS = 3,
98    PROJECT = 4,
99    CONSTRUCTOR_PROPERTY = 5,
100    KEEP_DTS = 6
101  }
102
103  let mCurrentExportedPropertySet: Set<string> = new Set<string>();
104  let mCurrentExportNameSet: Set<string> = new Set<string>();
105  export let mPropertySet: Set<string> = new Set<string>();
106  export let mExportNames: Set<string> = new Set<string>();
107  export let mConstructorPropertySet: Set<string> = undefined;
108  export let mSystemExportSet: Set<string> = new Set<string>();
109  /**
110   * filter classes or interfaces with export, default, etc
111   */
112  const getKeyword = function (modifiers: ModifiersArray): KeywordInfo {
113    if (modifiers === undefined) {
114      return {hasExport: false, hasDeclare: false};
115    }
116
117    let hasExport: boolean = false;
118    let hasDeclare: boolean = false;
119
120    for (const modifier of modifiers) {
121      if (modifier.kind === SyntaxKind.ExportKeyword) {
122        hasExport = true;
123      }
124
125      if (modifier.kind === SyntaxKind.DeclareKeyword) {
126        hasDeclare = true;
127      }
128    }
129
130    return {hasExport: hasExport, hasDeclare: hasDeclare};
131  };
132
133  /**
134   * get export name list
135   * @param astNode
136   */
137  const visitExport = function (astNode, isSystemApi: boolean): void {
138    /**
139     * export = exportClass //collect exportClass
140     *
141     * function foo()
142     * export default foo //collect foo
143     */
144    if (isExportAssignment(astNode)) {
145      let nodeName = astNode.expression.getText();
146      if (!mCurrentExportNameSet.has(nodeName)) {
147        collectNodeName(nodeName);
148      }
149      return;
150    }
151
152    if (isExportDeclaration(astNode) && astNode.exportClause) {
153      /**
154       * export {name1, name2} //collect name1, name2
155       * export {name1 as n1, name2} //collect n1, name2
156       * export {name1 as default, name2, name3} //collect default, name2, name3
157       */
158      if (isNamedExports(astNode.exportClause)) {
159        for (const element of astNode.exportClause.elements) {
160          const exportElementName = element.name.getText();
161          if (!mCurrentExportNameSet.has(exportElementName)) {
162            collectNodeName(exportElementName);
163          }
164        }
165      }
166
167      /**
168       * export * as name1 from 'file.ts' //collect name1
169       */
170      if (isNamespaceExport(astNode.exportClause)) {
171        const exportElementName = astNode.exportClause.name.getText();
172        if (!mCurrentExportNameSet.has(exportElementName)) {
173          collectNodeName(exportElementName);
174        }
175      }
176
177      /**
178      * Other export syntax, which does not contain a name. such as:
179      * export * from 'file.ts'
180      */
181      return;
182    }
183
184    let {hasExport, hasDeclare} = getKeyword(astNode.modifiers);
185    if (!hasExport) {
186      addCommonJsExports(astNode, isSystemApi);
187      return;
188    }
189
190    if (astNode.name) {
191      let nodeName = astNode.name.getText();
192      if (!mCurrentExportNameSet.has(nodeName)) {
193        collectNodeName(nodeName);
194      }
195
196      return;
197    }
198
199    if (hasDeclare && astNode.declarationList) {
200      astNode.declarationList.declarations.forEach((declaration) => {
201        const declarationName = declaration.name.getText();
202        if (!mCurrentExportNameSet.has(declarationName)) {
203          collectNodeName(declarationName);
204        }
205      });
206    }
207  };
208
209  const isCollectedToplevelElements = function (astNode): boolean {
210    if (astNode.name && !mCurrentExportNameSet.has(astNode.name.getText())) {
211      return false;
212    }
213
214    if (astNode.name === undefined) {
215      let {hasDeclare} = getKeyword(astNode.modifiers);
216      if (hasDeclare && astNode.declarationList &&
217        !mCurrentExportNameSet.has(astNode.declarationList.declarations[0].name.getText())) {
218        return false;
219      }
220    }
221
222    return true;
223  };
224
225  /**
226   * used only in oh sdk api extract or api of xxx.d.ts declaration file
227   * @param astNode
228   */
229  const visitChildNode = function (astNode): void {
230    if (!astNode) {
231      return;
232    }
233
234    if (astNode.name !== undefined && !mCurrentExportedPropertySet.has(astNode.name.getText())) {
235      if (isStringLiteral(astNode.name)) {
236        mCurrentExportedPropertySet.add(astNode.name.text);
237      } else {
238        mCurrentExportedPropertySet.add(astNode.name.getText());
239      }
240    }
241
242    astNode.forEachChild((childNode) => {
243      visitChildNode(childNode);
244    });
245  };
246
247  // Collect constructor properties from all files.
248  const visitNodeForConstructorProperty = function (astNode): void {
249    if (!astNode) {
250      return;
251    }
252
253    if (isConstructorDeclaration) {
254      const visitParam = (param: ParameterDeclaration): void => {
255        const modifiers = getModifiers(param);
256        if (!modifiers || modifiers.length <= 0) {
257          return;
258        }
259
260        const findRet = modifiers.find(modifier => isParameterPropertyModifier(modifier));
261        if (!isIdentifier(param.name) || findRet === undefined) {
262          return;
263        }
264        mConstructorPropertySet?.add(param.name.getText());
265      };
266
267      astNode?.parameters?.forEach((param) => {
268        visitParam(param);
269      });
270    }
271
272    astNode.forEachChild((childNode) => {
273      visitNodeForConstructorProperty(childNode);
274    });
275  };
276  /**
277   * visit ast of a file and collect api list
278   * used only in oh sdk api extract
279   * @param astNode node of ast
280   */
281  const visitPropertyAndName = function (astNode): void {
282    if (!isCollectedToplevelElements(astNode)) {
283      /**
284       * Collects property names of elements within top-level elements that haven't been collected yet.
285       * @param astNode toplevel elements of sourcefile
286       */
287      collectPropertyNames(astNode);
288      return;
289    }
290
291    visitChildNode(astNode);
292  };
293
294  /**
295   * commonjs exports extract
296   * examples:
297   * - exports.A = 1;
298   * - exports.B = hello; // hello can be variable or class ...
299   * - exports.C = {};
300   * - exports.D = class {};
301   * - exports.E = function () {}
302   * - class F {}
303   * - exports.F = F;
304   * - module.exports = {G: {}};
305   */
306  const addCommonJsExports = function (astNode: Node, isRemoteHarOrSystemApi: boolean = false): void {
307    if (!isExpressionStatement(astNode) || !astNode.expression) {
308      return;
309    }
310
311    const expression = astNode.expression;
312    if (!isBinaryExpression(expression)) {
313      return;
314    }
315
316    const left = expression.left;
317    if (!isElementAccessExpression(left) && !isPropertyAccessExpression(left)) {
318      return;
319    }
320
321    if (!isModuleExports(left) || expression.operatorToken.kind !== SyntaxKind.EqualsToken) {
322      return;
323    }
324
325    if (isElementAccessExpression(left)) {
326      if (isStringLiteral(left.argumentExpression)) {
327        /**
328         * - module.exports['A'] = class {};
329         * - module.exports['a'] = {};
330         * - module.exports['a'] = A;
331         */
332        mCurrentExportedPropertySet.add(left.argumentExpression.text);
333        mCurrentExportNameSet.add(left.argumentExpression.text);
334      }
335    }
336
337    if (isPropertyAccessExpression(left)) {
338      if (isIdentifier(left.name)) {
339        /**
340         * - module.exports.A = a;
341         * - module.exports.A = {};
342         * - module.exports.A = class {};
343         */
344        mCurrentExportedPropertySet.add(left.name.getText());
345        mCurrentExportNameSet.add(left.name.getText());
346      }
347    }
348
349    if (isIdentifier(expression.right)) {
350      /**
351       * module.exports.A = a;
352       * exports.A = a;
353       * module.exports = a;
354       */
355      let originalName = expression.right.getText();
356      if (isRemoteHarOrSystemApi) {
357        // To achieve compatibility changes, originalName is still collected into mCurrentExportNameSet
358        // for both remoteHar and system API files.
359
360        // NOTE: This logic will be optimized later to avoid collecting originalName into mCurrentExportNameSet under any circumstances.
361        mCurrentExportNameSet.add(originalName);
362      } else {
363        exportOriginalNameSet.add(originalName);
364      }
365      return;
366    }
367
368    if (isClassDeclaration(expression.right) || isClassExpression(expression.right)) {
369      /**
370       * module.exports.A = class testClass {}
371       * module.exports = class testClass {}
372       * exports.A = class testClass {}
373       * module.exports.A = class {}
374       */
375      getClassProperties(expression.right, mCurrentExportedPropertySet);
376      return;
377    }
378
379    if (isObjectLiteralExpression(expression.right)) {
380      /**
381       * module.exports = {a, b, c};
382       * module.exports.A = {a, b, c};
383       * exports.A = {a, b, c}
384       */
385      getObjectProperties(expression.right, mCurrentExportedPropertySet);
386      // module.exports = {a, b, c};
387      let defaultExport = left.expression.getText() === 'module';
388      if (defaultExport) {
389        getObjectExportNames(expression.right, mCurrentExportNameSet);
390      }
391      return;
392    }
393
394    return;
395  };
396
397  function isModuleExports(leftExpression: ElementAccessExpression | PropertyAccessExpression): boolean {
398    let leftExpressionText = leftExpression.expression.getText();
399    if (isPropertyAccessExpression(leftExpression.expression)) {
400      // module.exports.a = A;
401      // module.exports['a'] = A;
402      return leftExpressionText === 'module.exports';
403    }
404    if (isIdentifier(leftExpression.expression)) {
405      if (leftExpressionText === 'module') {
406        // module.exports = {A};
407        if (isPropertyAccessExpression(leftExpression) && leftExpression.name.getText() === 'exports') {
408          return true;
409        }
410      }
411
412      // exports.a = A;
413      return leftExpressionText === 'exports';
414    }
415    return false;
416  };
417
418  /**
419   * extract project export name
420   * - export {xxx, xxx};
421   * - export {xxx as xx, xxx as xx};
422   * - export default function/class/...{};
423   * - export class xxx{}
424   * - ...
425   * @param astNode
426   */
427  const visitProjectExport = function (astNode, isRemoteHarFile: boolean): void {
428    if (isExportAssignment(astNode)) {
429      handleExportAssignment(astNode);
430      return;
431    }
432
433    if (isExportDeclaration(astNode)) {
434      handleExportDeclaration(astNode, isRemoteHarFile);
435      return;
436    }
437
438    let {hasExport} = getKeyword(astNode.modifiers);
439    if (!hasExport) {
440      addCommonJsExports(astNode, isRemoteHarFile);
441      forEachChild(astNode, node => visitProjectExport(node, isRemoteHarFile));
442      return;
443    }
444
445    if (astNode.name) {
446      if (!mCurrentExportNameSet.has(astNode.name.getText())) {
447        mCurrentExportNameSet.add(astNode.name.getText());
448        mCurrentExportedPropertySet.add(astNode.name.getText());
449      }
450
451      forEachChild(astNode, node => visitProjectExport(node, isRemoteHarFile));
452      return;
453    }
454
455    if (isClassDeclaration(astNode)) {
456      getClassProperties(astNode, mCurrentExportedPropertySet);
457      return;
458    }
459
460    if (isVariableStatement(astNode)) {
461      astNode.declarationList.forEachChild((child) => {
462        if (isVariableDeclaration(child) && !mCurrentExportNameSet.has(child.name.getText())) {
463          mCurrentExportNameSet.add(child.name.getText());
464          mCurrentExportedPropertySet.add(child.name.getText());
465        }
466      });
467
468      return;
469    }
470
471    forEachChild(astNode, node => visitProjectExport(node, isRemoteHarFile));
472  };
473
474  function handleExportAssignment(astNode): void {
475    // let xxx; export default xxx = a;
476    if (isBinaryExpression(astNode.expression)) {
477      if (isObjectLiteralExpression(astNode.expression.right)) {
478        getObjectProperties(astNode.expression.right, mCurrentExportedPropertySet);
479        return;
480      }
481
482      if (isClassExpression(astNode.expression.right)) {
483        getClassProperties(astNode.expression.right, mCurrentExportedPropertySet);
484      }
485
486      return;
487    }
488
489    // export = xxx; The xxx here can't be obfuscated
490    // export default yyy; The yyy here can be obfuscated
491    if (isIdentifier(astNode.expression)) {
492      if (!mCurrentExportNameSet.has(astNode.expression.getText())) {
493        mCurrentExportNameSet.add(astNode.expression.getText());
494        mCurrentExportedPropertySet.add(astNode.expression.getText());
495      }
496      return;
497    }
498
499    if (isObjectLiteralExpression(astNode.expression)) {
500      getObjectProperties(astNode.expression, mCurrentExportedPropertySet);
501    }
502  }
503
504  function handleExportDeclaration(astNode: ExportDeclaration, isRemoteHarFile: boolean): void {
505    if (astNode.exportClause) {
506      if (astNode.exportClause.kind === SyntaxKind.NamedExports) {
507        astNode.exportClause.forEachChild((child) => {
508          if (!isExportSpecifier(child)) {
509            return;
510          }
511
512          if (child.propertyName) {
513            let originalName = child.propertyName.getText();
514            if (isRemoteHarFile || astNode.moduleSpecifier) {
515              // For the first condition, this ensures that for remoteHar files,
516              // originalName is still collected into mCurrentExportNameSet to maintain compatibility.
517              // NOTE: This specification needs to be revised to determine whether to add originalName
518              // to mCurrentExportNameSet should be independent of whether it is in a remoteHar file.
519
520              // The second condition indicates that for `export {A as B} from './filePath'` statements,
521              // the original name (A) needs to be added to the export whitelist.
522              mCurrentExportNameSet.add(originalName);
523            } else {
524              /**
525               * In project source code:
526               * class A {
527               *   prop1 = 1;
528               *   prop2 = 2;
529               * }
530               * export {A as B}; // collect A to ensure we can collect prop1 and prop2
531               */
532              exportOriginalNameSet.add(originalName);
533            }
534          }
535
536          let exportName = child.name.getText();
537          mCurrentExportedPropertySet.add(exportName);
538          mCurrentExportNameSet.add(exportName);
539        });
540      }
541
542      if (astNode.exportClause.kind === SyntaxKind.NamespaceExport) {
543        mCurrentExportedPropertySet.add(astNode.exportClause.name.getText());
544        return;
545      }
546    }
547  }
548
549  /**
550   * extract the class, enum, and object properties of the export in the project before obfuscation
551   * class A{};
552   * export = A; need to be considered
553   * export = namespace;
554   * This statement also needs to determine whether there is an export in the namespace, and namespaces are also allowed in the namespace
555   * @param astNode
556   */
557  const visitProjectNode = function (astNode): void {
558    const currentPropsSet: Set<string> = new Set();
559    let nodeName: string | undefined = astNode.name?.text;
560    if ((isClassDeclaration(astNode) || isStructDeclaration(astNode))) {
561      getClassProperties(astNode, currentPropsSet);
562    } else if (isEnumDeclaration(astNode)) { // collect export enum structure properties
563      getEnumProperties(astNode, currentPropsSet);
564    } else if (isVariableDeclaration(astNode)) {
565      if (astNode.initializer) {
566        if (isObjectLiteralExpression(astNode.initializer)) {
567          getObjectProperties(astNode.initializer, currentPropsSet);
568        } else if (isClassExpression(astNode.initializer)) {
569          getClassProperties(astNode.initializer, currentPropsSet);
570        }
571      }
572      nodeName = astNode.name?.getText();
573    } else if (isInterfaceDeclaration(astNode)) {
574      getInterfaceProperties(astNode, currentPropsSet);
575    } else if (isTypeAliasDeclaration(astNode)) {
576      getTypeAliasProperties(astNode, currentPropsSet);
577    } else if (isElementAccessExpression(astNode)) {
578      getElementAccessExpressionProperties(astNode, currentPropsSet);
579    } else if (isObjectLiteralExpression(astNode)) {
580      getObjectProperties(astNode, currentPropsSet);
581    } else if (isClassExpression(astNode)) {
582      getClassProperties(astNode, currentPropsSet);
583    }
584
585    addPropWhiteList(nodeName, astNode, currentPropsSet);
586
587    forEachChild(astNode, visitProjectNode);
588  };
589
590  function addPropWhiteList(nodeName: string | undefined, astNode: Node, currentPropsSet: Set<string>): void {
591    if (nodeName && (mCurrentExportNameSet.has(nodeName) || exportOriginalNameSet.has(nodeName))) {
592      addElement(currentPropsSet);
593    }
594
595    if (scanProjectConfig.isHarCompiled && scanProjectConfig.mPropertyObfuscation && isEnumDeclaration(astNode)) {
596      addEnumElement(currentPropsSet);
597    }
598  }
599
600  function addElement(currentPropsSet: Set<string>): void {
601    currentPropsSet.forEach((element: string) => {
602      mCurrentExportedPropertySet.add(element);
603    });
604  }
605
606  function addEnumElement(currentPropsSet: Set<string>): void {
607    currentPropsSet.forEach((element: string) => {
608      enumPropsSet.add(element);
609    });
610  }
611  /**
612   * parse file to api list and save to json object
613   * @param fileName file name of api file
614   * @param apiType
615   * @private
616   */
617  const parseFile = function (fileName: string, apiType: ApiType): void {
618    if (!FileUtils.isReadableFile(fileName) || !isParsableFile(fileName)) {
619      return;
620    }
621
622    const sourceFile: SourceFile = createSourceFile(fileName, fs.readFileSync(fileName).toString(), ScriptTarget.ES2015, true);
623    mCurrentExportedPropertySet.clear();
624    // get export name list
625    switch (apiType) {
626      case ApiType.COMPONENT:
627      case ApiType.KEEP_DTS:
628        forEachChild(sourceFile, visitChildNode);
629        break;
630      case ApiType.API:
631        mCurrentExportNameSet.clear();
632        forEachChild(sourceFile, node => visitExport(node, true));
633        mCurrentExportNameSet.forEach(item => mSystemExportSet.add(item));
634
635        forEachChild(sourceFile, visitPropertyAndName);
636        mCurrentExportNameSet.clear();
637        break;
638      case ApiType.PROJECT_DEPENDS:
639      case ApiType.PROJECT:
640        mCurrentExportNameSet.clear();
641        if (fileName.endsWith('.d.ts') || fileName.endsWith('.d.ets')) {
642          forEachChild(sourceFile, visitChildNode);
643        }
644
645        let isRemoteHarFile = isRemoteHar(fileName);
646        forEachChild(sourceFile, node => visitProjectExport(node, isRemoteHarFile));
647        forEachChild(sourceFile, visitProjectNode);
648        mCurrentExportedPropertySet = handleWhiteListWhenExportObfs(fileName, mCurrentExportedPropertySet);
649        mCurrentExportNameSet = handleWhiteListWhenExportObfs(fileName, mCurrentExportNameSet);
650        break;
651      case ApiType.CONSTRUCTOR_PROPERTY:
652        forEachChild(sourceFile, visitNodeForConstructorProperty);
653        break;
654      default:
655        break;
656    }
657
658    // collect export names.
659    mCurrentExportNameSet.forEach(item => mExportNames.add(item));
660    mCurrentExportNameSet.clear();
661    // collect export names and properties.
662    mCurrentExportedPropertySet.forEach(item => mPropertySet.add(item));
663    mCurrentExportedPropertySet.clear();
664    exportOriginalNameSet.clear();
665  };
666
667  function handleWhiteListWhenExportObfs(fileName: string, collectedExportNamesAndProperties: Set<string>): Set<string> {
668    // If mExportObfuscation is not enabled, collect the export names and their properties into the whitelist.
669    if (!scanProjectConfig.mExportObfuscation) {
670      return collectedExportNamesAndProperties;
671    }
672    // If the current file is a keep file or its dependent file, collect the export names and their properties into the whitelist.
673    if (scanProjectConfig.mkeepFilesAndDependencies?.has(fileName)) {
674      return collectedExportNamesAndProperties;
675    }
676    // If it is a project source code file, the names and their properties of the export will not be collected.
677    if (!isRemoteHar(fileName)) {
678      collectedExportNamesAndProperties.clear();
679      return collectedExportNamesAndProperties;
680    }
681    // If it is a third-party library file.
682    return collectedExportNamesAndProperties;
683  }
684
685  const projectExtensions: string[] = ['.ets', '.ts', '.js'];
686  const projectDependencyExtensions: string[] = ['.d.ets', '.d.ts', '.ets', '.ts', '.js'];
687  const resolvedModules = new Set();
688
689  function tryGetPackageID(filePath: string): string {
690    const ohPackageJsonPath = path.join(filePath, 'oh-package.json5');
691    let packgeNameAndVersion = '';
692    if (fs.existsSync(ohPackageJsonPath)) {
693      const ohPackageContent = json5.parse(fs.readFileSync(ohPackageJsonPath, 'utf-8'));
694      packgeNameAndVersion = ohPackageContent.name + ohPackageContent.version;
695    }
696    return packgeNameAndVersion;
697  }
698
699  function traverseFilesInDir(apiPath: string, apiType: ApiType): void {
700    let fileNames: string[] = fs.readdirSync(apiPath);
701    for (let fileName of fileNames) {
702      let filePath: string = path.join(apiPath, fileName);
703      try {
704        fs.accessSync(filePath, fs.constants.R_OK);
705      } catch (err) {
706        continue;
707      }
708      if (fs.statSync(filePath).isDirectory()) {
709        const packgeNameAndVersion = tryGetPackageID(filePath);
710        if (resolvedModules.has(packgeNameAndVersion)) {
711          continue;
712        }
713        traverseApiFiles(filePath, apiType);
714        packgeNameAndVersion.length > 0 && resolvedModules.add(packgeNameAndVersion);
715        continue;
716      }
717      const suffix: string = path.extname(filePath);
718      if ((apiType !== ApiType.PROJECT) && !projectDependencyExtensions.includes(suffix)) {
719        continue;
720      }
721
722      if (apiType === ApiType.PROJECT && !projectExtensions.includes(suffix)) {
723        continue;
724      }
725      parseFile(filePath, apiType);
726    }
727  }
728
729  /**
730   * traverse files of  api directory
731   * @param apiPath api directory path
732   * @param apiType
733   * @private
734   */
735  export const traverseApiFiles = function (apiPath: string, apiType: ApiType): void {
736    if (fs.statSync(apiPath).isDirectory()) {
737      traverseFilesInDir(apiPath, apiType);
738    } else {
739      parseFile(apiPath, apiType);
740    }
741  };
742
743  /**
744   * desc: parse openHarmony sdk to get api list
745   * @param version version of api, e.g. version 5.0.1.0 for api 9
746   * @param sdkPath sdk real path of openHarmony
747   * @param isEts true for ets, false for js
748   * @param outputDir: sdk api output directory
749   */
750  export function parseOhSdk(sdkPath: string, version: string, isEts: boolean, outputDir: string): void {
751    mPropertySet.clear();
752
753    // visit api directory
754    const apiPath: string = path.join(sdkPath, (isEts ? 'ets' : 'js'), version, 'api');
755    traverseApiFiles(apiPath, ApiType.API);
756
757    // visit component directory if ets
758    if (isEts) {
759      const componentPath: string = path.join(sdkPath, 'ets', version, 'component');
760      traverseApiFiles(componentPath, ApiType.COMPONENT);
761    }
762
763    // visit the UI conversion API
764    const uiConversionPath: string = path.join(sdkPath, (isEts ? 'ets' : 'js'), version,
765      'build-tools', 'ets-loader', 'lib', 'pre_define.js');
766    extractStringsFromFile(uiConversionPath);
767
768    const reservedProperties: string[] = [...mPropertySet.values()];
769    mPropertySet.clear();
770
771    writeToFile(reservedProperties, path.join(outputDir, 'propertiesReserved.json'));
772  }
773
774  export function extractStringsFromFile(filePath: string): void {
775    let collections: string[] = [];
776    const fileContent = fs.readFileSync(filePath, 'utf-8');
777    const regex = /"([^"]*)"/g;
778    const matches = fileContent.match(regex);
779
780    if (matches) {
781      collections = matches.map(match => match.slice(1, -1));
782    }
783
784    collections.forEach(name => mPropertySet.add(name));
785  }
786
787  /**
788   * parse common project or file to extract exported api list
789   * @return reserved api names
790   */
791  export function parseCommonProject(projectPath: string, customProfiles: IOptions, scanningApiType: ApiType): string[] {
792    mPropertySet.clear();
793    if (fs.lstatSync(projectPath).isFile()) {
794      if (projectPath.endsWith('.ets') || projectPath.endsWith('.ts') || projectPath.endsWith('.js')) {
795        parseFile(projectPath, scanningApiType);
796      }
797    } else {
798      traverseApiFiles(projectPath, scanningApiType);
799    }
800
801    let reservedProperties: string[] = [...mPropertySet];
802    mPropertySet.clear();
803    return reservedProperties;
804  }
805
806  /**
807   * only for ut
808   * parse api of third party libs like libs in node_modules
809   * @param libPath
810   */
811  export function parseThirdPartyLibs(libPath: string, scanningApiType: ApiType): {reservedProperties: string[]; reservedLibExportNames: string[] | undefined} {
812    mPropertySet.clear();
813    mExportNames.clear();
814    if (fs.lstatSync(libPath).isFile()) {
815      if (libPath.endsWith('.ets') || libPath.endsWith('.ts') || libPath.endsWith('.js')) {
816        parseFile(libPath, scanningApiType);
817      }
818    } else {
819      const filesAndfolders = fs.readdirSync(libPath);
820      for (let subPath of filesAndfolders) {
821        traverseApiFiles(path.join(libPath, subPath), scanningApiType);
822      }
823    }
824    let reservedLibExportNames: string[] = undefined;
825    if (scanProjectConfig.mExportObfuscation) {
826      reservedLibExportNames = [...mExportNames];
827      mExportNames.clear();
828    }
829    const reservedProperties: string[] = [...mPropertySet];
830    mPropertySet.clear();
831
832    return {reservedProperties: reservedProperties, reservedLibExportNames: reservedLibExportNames};
833  }
834
835  /**
836   * save api json object to file
837   * @private
838   */
839  export function writeToFile(reservedProperties: string[], outputPath: string): void {
840    let str: string = JSON.stringify(reservedProperties, null, '\t');
841    fs.writeFileSync(outputPath, str);
842  }
843
844  export function isRemoteHar(filePath: string): boolean {
845    const realPath: string = sys.realpath(filePath);
846    return isInOhModuleFile(realPath);
847  }
848
849  export function isInOhModuleFile(filePath: string): boolean {
850    return filePath.indexOf('/oh_modules/') !== -1 || filePath.indexOf('\\oh_modules\\') !== -1;
851  }
852
853  export function isParsableFile(path: string): boolean {
854    return supportedParsingExtension.some(extension => path.endsWith(extension));
855  }
856
857  /**
858  * parse common project or file to extract exported api list
859  * @return reserved api names
860  */
861  export function parseFileByPaths(projectPaths: Set<string>, scanningApiType: ApiType):
862    {reservedProperties: string[]; reservedExportNames: string[]} {
863    mPropertySet.clear();
864    mExportNames.clear();
865    projectPaths.forEach(path => {
866      parseFile(path, scanningApiType);
867    });
868    let reservedProperties: string[] = [];
869    let reservedExportNames: string[] = [];
870    if (scanProjectConfig.mPropertyObfuscation) {
871      reservedProperties = [...mPropertySet];
872    }
873    if (scanProjectConfig.mExportObfuscation) {
874      reservedExportNames = [...mExportNames];
875    }
876    mPropertySet.clear();
877    mExportNames.clear();
878    return {reservedProperties: reservedProperties, reservedExportNames: reservedExportNames};
879  }
880
881  /**
882   * Collect all property names in the AST.
883   * @param astNode Nodes of the AST.
884   */
885  function collectPropertyNames(astNode: Node): void {
886    visitElementsWithProperties(astNode);
887  }
888
889  /**
890   * Visit elements that can contain properties.
891   * @param node The current AST node.
892   */
893  function visitElementsWithProperties(node: Node): void {
894    switch (node.kind) {
895      case SyntaxKind.ClassDeclaration:
896        forEachChild(node, visitClass);
897        break;
898      case SyntaxKind.InterfaceDeclaration:
899      case SyntaxKind.TypeLiteral:
900        forEachChild(node, visitInterfaceOrType);
901        break;
902      case SyntaxKind.EnumDeclaration:
903        forEachChild(node, visitEnum);
904        break;
905      case SyntaxKind.ObjectLiteralExpression:
906        forEachChild(node, visitObjectLiteral);
907        break;
908      case SyntaxKind.ModuleDeclaration:
909        forEachChild(node, visitModule);
910        break;
911    }
912    forEachChild(node, visitElementsWithProperties);
913  }
914
915  function visitClass(node: Node): void {
916    if (isPropertyDeclaration(node) || isMethodDeclaration(node)) {
917      if (isIdentifier(node.name)) {
918        mCurrentExportedPropertySet.add(node.name.text);
919      }
920    }
921    forEachChild(node, visitClass);
922  }
923
924  function visitInterfaceOrType(node: Node): void {
925    if (isPropertySignature(node) || isMethodSignature(node)) {
926      if (isIdentifier(node.name)) {
927        mCurrentExportedPropertySet.add(node.name.text);
928      }
929    }
930    forEachChild(node, visitInterfaceOrType);
931  }
932
933  function visitEnum(node: Node): void {
934    if (isEnumMember(node) && isIdentifier(node.name)) {
935      mCurrentExportedPropertySet.add(node.name.text);
936    }
937  }
938
939  function visitObjectLiteral(node: Node): void {
940    if (isPropertyAssignment(node)) {
941      if (isIdentifier(node.name)) {
942        mCurrentExportedPropertySet.add(node.name.text);
943      }
944    }
945    forEachChild(node, visitObjectLiteral);
946  }
947
948  function visitModule(node: Node): void {
949    forEachChild(node, visitElementsWithProperties);
950  }
951
952  function collectNodeName(name: string): void {
953    mCurrentExportNameSet.add(name);
954    mCurrentExportedPropertySet.add(name);
955  }
956}
957