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