• 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  ClassDeclaration,
18  CommentRange,
19  ElementAccessExpression,
20  EnumDeclaration,
21  ExportDeclaration,
22  FunctionDeclaration,
23  InterfaceDeclaration,
24  ModifiersArray,
25  ModuleDeclaration,
26  Node,
27  ParameterDeclaration,
28  PropertyAccessExpression,
29  SourceFile,
30  TypeAliasDeclaration,
31  VariableDeclaration,
32  VariableStatement
33} from 'typescript';
34
35import {
36  createSourceFile,
37  ClassElement,
38  forEachChild,
39  getLeadingCommentRangesOfNode,
40  isBinaryExpression,
41  isClassDeclaration,
42  isClassExpression,
43  isExpressionStatement,
44  isEnumDeclaration,
45  isExportAssignment,
46  isExportDeclaration,
47  isExportSpecifier,
48  isGetAccessor,
49  isIdentifier,
50  isInterfaceDeclaration,
51  isModuleBlock,
52  isObjectLiteralExpression,
53  isParameterPropertyDeclaration,
54  isStructDeclaration,
55  isSourceFile,
56  isTypeAliasDeclaration,
57  isVariableDeclaration,
58  isVariableStatement,
59  isElementAccessExpression,
60  isPropertyAccessExpression,
61  isSetAccessor,
62  isStringLiteral,
63  ScriptTarget,
64  SyntaxKind,
65  sys,
66  isConstructorDeclaration,
67  getModifiers,
68  isNamedExports,
69  isNamespaceExport,
70  isPropertyDeclaration,
71  isPropertySignature,
72  isMethodDeclaration,
73  isMethodSignature,
74  isPropertyAssignment,
75  isEnumMember,
76  isParameter,
77  isTypeParameterDeclaration,
78  isIndexedAccessTypeNode,
79  Extension
80} from 'typescript';
81
82import fs from 'fs';
83import path from 'path';
84import json5 from 'json5';
85
86import {
87  exportOriginalNameSet,
88  getClassProperties,
89  getElementAccessExpressionProperties,
90  getEnumProperties,
91  getIndexedAccessTypeProperties,
92  getInterfaceProperties,
93  getObjectExportNames,
94  getObjectProperties,
95  getTypeAliasProperties,
96  isParameterPropertyModifier,
97} from '../utils/OhsUtil';
98import { scanProjectConfig } from './ApiReader';
99import { enumPropsSet } from '../utils/OhsUtil';
100import { FileUtils } from '../utils/FileUtils';
101import { supportedParsingExtension } from './type';
102import { addToSet, FileWhiteList, KeepInfo, projectWhiteListManager } from '../utils/ProjectCollections';
103import { AtKeepCollections } from '../utils/CommonCollections';
104import { hasExportModifier } from '../utils/NodeUtils';
105
106export namespace ApiExtractor {
107  interface KeywordInfo {
108    hasExport: boolean,
109    hasDeclare: boolean
110  }
111
112  export enum ApiType {
113    API,
114    COMPONENT,
115    PROJECT,
116    CONSTRUCTOR_PROPERTY,
117    KEEP_DTS
118  }
119
120  export enum AtKeepType {
121    None,
122    KeepSymbol,
123    KeepAsConsumer
124  }
125
126  export enum WhiteListType {
127    PropertyName,
128    GlobalName,
129  }
130
131  type KeepTargetNode =
132  | ClassDeclaration
133  | InterfaceDeclaration
134  | EnumDeclaration
135  | FunctionDeclaration
136  | ModuleDeclaration
137  | VariableDeclaration
138  | TypeAliasDeclaration;
139
140  const KEEP_SYMBOL = '//@KeepSymbol';
141  const KEEP_AS_CONSUMER = '//@KeepAsConsumer';
142
143  let mCurrentExportedPropertySet: Set<string> = new Set<string>();
144  let mCurrentExportNameSet: Set<string> = new Set<string>();
145
146  let keepSymbolTemp: KeepInfo = {
147    propertyNames: new Set<string>(),
148    globalNames: new Set<string>(),
149  };
150  let keepAsConsumerTemp: KeepInfo = {
151    propertyNames: new Set<string>(),
152    globalNames: new Set<string>(),
153  };
154
155  function clearAtKeepTemp(): void {
156    keepSymbolTemp.propertyNames.clear();
157    keepSymbolTemp.globalNames.clear();
158    keepAsConsumerTemp.propertyNames.clear();
159    keepAsConsumerTemp.globalNames.clear();
160  }
161
162  export let mPropertySet: Set<string> = new Set<string>();
163  export let mExportNames: Set<string> = new Set<string>();
164  export let mConstructorPropertySet: Set<string> = new Set<string>();
165  export let mEnumMemberSet: Set<string> = new Set<string>();
166  export let mSystemExportSet: Set<string> = new Set<string>();
167  /**
168   * filter classes or interfaces with export, default, etc
169   */
170  const getKeyword = function (modifiers: ModifiersArray): KeywordInfo {
171    if (modifiers === undefined) {
172      return {hasExport: false, hasDeclare: false};
173    }
174
175    let hasExport: boolean = false;
176    let hasDeclare: boolean = false;
177
178    for (const modifier of modifiers) {
179      if (modifier.kind === SyntaxKind.ExportKeyword) {
180        hasExport = true;
181      }
182
183      if (modifier.kind === SyntaxKind.DeclareKeyword) {
184        hasDeclare = true;
185      }
186    }
187
188    return {hasExport: hasExport, hasDeclare: hasDeclare};
189  };
190
191  /**
192   * get export name list
193   * @param astNode
194   */
195  const visitExport = function (astNode, isSystemApi: boolean): void {
196    /**
197     * export = exportClass //collect exportClass
198     *
199     * function foo()
200     * export default foo //collect foo
201     */
202    if (isExportAssignment(astNode)) {
203      let nodeName = astNode.expression.getText();
204      if (!mCurrentExportNameSet.has(nodeName)) {
205        collectNodeName(nodeName);
206      }
207      return;
208    }
209
210    if (isExportDeclaration(astNode) && astNode.exportClause) {
211      /**
212       * export {name1, name2} //collect name1, name2
213       * export {name1 as n1, name2} //collect n1, name2
214       * export {name1 as default, name2, name3} //collect default, name2, name3
215       */
216      if (isNamedExports(astNode.exportClause)) {
217        for (const element of astNode.exportClause.elements) {
218          const exportElementName = element.name.getText();
219          if (!mCurrentExportNameSet.has(exportElementName)) {
220            collectNodeName(exportElementName);
221          }
222        }
223      }
224
225      /**
226       * export * as name1 from 'file.ts' //collect name1
227       */
228      if (isNamespaceExport(astNode.exportClause)) {
229        const exportElementName = astNode.exportClause.name.getText();
230        if (!mCurrentExportNameSet.has(exportElementName)) {
231          collectNodeName(exportElementName);
232        }
233      }
234
235      /**
236      * Other export syntax, which does not contain a name. such as:
237      * export * from 'file.ts'
238      */
239      return;
240    }
241
242    let {hasExport, hasDeclare} = getKeyword(astNode.modifiers);
243    if (!hasExport) {
244      return;
245    }
246
247    if (astNode.name) {
248      let nodeName = astNode.name.getText();
249      if (!mCurrentExportNameSet.has(nodeName)) {
250        collectNodeName(nodeName);
251      }
252
253      return;
254    }
255
256    if (hasDeclare && astNode.declarationList) {
257      astNode.declarationList.declarations.forEach((declaration) => {
258        const declarationName = declaration.name.getText();
259        if (!mCurrentExportNameSet.has(declarationName)) {
260          collectNodeName(declarationName);
261        }
262      });
263    }
264  };
265
266  const isCollectedExportNames = function (astNode): boolean {
267    if (astNode.name && !mCurrentExportNameSet.has(astNode.name.getText())) {
268      return false;
269    }
270
271    if (astNode.name === undefined) {
272      let {hasDeclare} = getKeyword(astNode.modifiers);
273      if (hasDeclare && astNode.declarationList &&
274        !mCurrentExportNameSet.has(astNode.declarationList.declarations[0].name.getText())) {
275        return false;
276      }
277    }
278
279    return true;
280  };
281
282  /**
283   * used only in oh sdk api extract or api of xxx.d.ts declaration file
284   * @param astNode
285   */
286  const visitChildNode = function (astNode, isSdkApi: boolean = false): void {
287    if (!astNode) {
288      return;
289    }
290    if (astNode.name !== undefined && !mCurrentExportedPropertySet.has(astNode.name.getText())) {
291      const notAddParameter: boolean = scanProjectConfig.mStripSystemApiArgs && isSdkApi;
292      if (!notAddParameter || (!isParameter(astNode) && !isTypeParameterDeclaration(astNode))) {
293        const nameToAdd = isStringLiteral(astNode.name) ? astNode.name.text : astNode.name.getText();
294        mCurrentExportedPropertySet.add(nameToAdd);
295      }
296    }
297
298    astNode.forEachChild((childNode) => {
299      visitChildNode(childNode, isSdkApi);
300    });
301  };
302
303  // Collect constructor properties from all files.
304  // To avoid generating the same name as the constructor property when obfuscating identifier names.
305  const visitNodeForConstructorProperty = function (astNode): void {
306    if (!astNode) {
307      return;
308    }
309
310    if (isConstructorDeclaration(astNode)) {
311      const visitParam = (param: ParameterDeclaration): void => {
312        const modifiers = getModifiers(param);
313        if (!modifiers || modifiers.length <= 0) {
314          return;
315        }
316
317        const findRet = modifiers.find(modifier => isParameterPropertyModifier(modifier));
318        if (!isIdentifier(param.name) || findRet === undefined) {
319          return;
320        }
321        mConstructorPropertySet.add(param.name.getText());
322        projectWhiteListManager?.fileWhiteListInfo?.fileReservedInfo.propertyParams.add(param.name.getText());
323      };
324
325      astNode?.parameters?.forEach((param) => {
326        visitParam(param);
327      });
328    }
329
330    astNode.forEachChild((childNode) => {
331      visitNodeForConstructorProperty(childNode);
332    });
333  };
334  /**
335   * visit ast of a file and collect api list
336   * used only in oh sdk api extract
337   * @param astNode node of ast
338   */
339  const visitPropertyAndNameForSdk = function (astNode): void {
340    if (!isCollectedExportNames(astNode)) {
341      /**
342       * Collects property names of elements that haven't been collected yet.
343       * @param astNode elements of sourcefile
344       */
345      collectPropertyNames(astNode);
346      return;
347    }
348
349    visitChildNode(astNode, true);
350  };
351
352  /**
353   * commonjs exports extract
354   * examples:
355   * - exports.A = 1;
356   * - exports.B = hello; // hello can be variable or class ...
357   * - exports.C = {};
358   * - exports.D = class {};
359   * - exports.E = function () {}
360   * - class F {}
361   * - exports.F = F;
362   * - module.exports = {G: {}};
363   */
364  const addCommonJsExports = function (astNode: Node, isRemoteHarOrSystemApi: boolean = false): void {
365    if (!isExpressionStatement(astNode) || !astNode.expression) {
366      return;
367    }
368
369    const expression = astNode.expression;
370    if (!isBinaryExpression(expression)) {
371      return;
372    }
373
374    const left = expression.left;
375    if (!isElementAccessExpression(left) && !isPropertyAccessExpression(left)) {
376      return;
377    }
378
379    if (!isModuleExports(left) || expression.operatorToken.kind !== SyntaxKind.EqualsToken) {
380      return;
381    }
382
383    if (isElementAccessExpression(left)) {
384      if (isStringLiteral(left.argumentExpression)) {
385        /**
386         * - module.exports['A'] = class {};
387         * - module.exports['a'] = {};
388         * - module.exports['a'] = A;
389         */
390        mCurrentExportedPropertySet.add(left.argumentExpression.text);
391        mCurrentExportNameSet.add(left.argumentExpression.text);
392      }
393    }
394
395    if (isPropertyAccessExpression(left)) {
396      if (isIdentifier(left.name)) {
397        /**
398         * - module.exports.A = a;
399         * - module.exports.A = {};
400         * - module.exports.A = class {};
401         */
402        mCurrentExportedPropertySet.add(left.name.getText());
403        mCurrentExportNameSet.add(left.name.getText());
404      }
405    }
406
407    if (isIdentifier(expression.right)) {
408      /**
409       * module.exports.A = a;
410       * exports.A = a;
411       * module.exports = a;
412       */
413      let originalName = expression.right.getText();
414      if (isRemoteHarOrSystemApi) {
415        // To achieve compatibility changes, originalName is still collected into mCurrentExportNameSet
416        // for both remoteHar and system API files.
417
418        // NOTE: This logic will be optimized later to avoid collecting originalName into mCurrentExportNameSet under any circumstances.
419        mCurrentExportNameSet.add(originalName);
420      } else {
421        exportOriginalNameSet.add(originalName);
422      }
423      return;
424    }
425
426    if (isClassDeclaration(expression.right) || isClassExpression(expression.right)) {
427      /**
428       * module.exports.A = class testClass {}
429       * module.exports = class testClass {}
430       * exports.A = class testClass {}
431       * module.exports.A = class {}
432       */
433      getClassProperties(expression.right, mCurrentExportedPropertySet);
434      return;
435    }
436
437    if (isObjectLiteralExpression(expression.right)) {
438      /**
439       * module.exports = {a, b, c};
440       * module.exports.A = {a, b, c};
441       * exports.A = {a, b, c}
442       */
443      getObjectProperties(expression.right, mCurrentExportedPropertySet);
444      // module.exports = {a, b, c}, {a, b, c} as the export content of the module
445      let defaultExport = left.expression.getText() === 'module';
446      if (defaultExport) {
447        getObjectExportNames(expression.right, mCurrentExportNameSet);
448      }
449      return;
450    }
451
452    return;
453  };
454
455  function isModuleExports(leftExpression: ElementAccessExpression | PropertyAccessExpression): boolean {
456    let leftExpressionText = leftExpression.expression.getText();
457    if (isPropertyAccessExpression(leftExpression.expression)) {
458      /**
459       * For example:
460       * module.exports.a = A;
461       * module.exports['a'] = A;
462       */
463      return leftExpressionText === 'module.exports';
464    }
465    if (isIdentifier(leftExpression.expression)) {
466      if (leftExpressionText === 'module') {
467        // module.exports = {A}, A as the export content of the module
468        if (isPropertyAccessExpression(leftExpression) && leftExpression.name.getText() === 'exports') {
469          return true;
470        }
471      }
472
473      /**
474       * For example:
475       * exports.a = A;
476       */
477      return leftExpressionText === 'exports';
478    }
479    return false;
480  };
481
482  /**
483   * extract project export name
484   * - export {xxx, xxx};
485   * - export {xxx as xx, xxx as xx};
486   * - export default function/class/...{};
487   * - export class xxx{}
488   * - ...
489   * @param astNode
490   */
491  const visitProjectExport = function (astNode, isRemoteHarFile: boolean): void {
492    if (isExportAssignment(astNode)) {
493      handleExportAssignment(astNode);
494      return;
495    }
496
497    if (isExportDeclaration(astNode)) {
498      handleExportDeclaration(astNode, isRemoteHarFile);
499      return;
500    }
501
502    let {hasExport} = getKeyword(astNode.modifiers);
503    if (!hasExport) {
504      addCommonJsExports(astNode, isRemoteHarFile);
505      forEachChild(astNode, node => visitProjectExport(node, isRemoteHarFile));
506      return;
507    }
508
509    if (astNode.name) {
510      if (!mCurrentExportNameSet.has(astNode.name.getText())) {
511        mCurrentExportNameSet.add(astNode.name.getText());
512        mCurrentExportedPropertySet.add(astNode.name.getText());
513      }
514
515      forEachChild(astNode, node => visitProjectExport(node, isRemoteHarFile));
516      return;
517    }
518
519    if (isClassDeclaration(astNode)) {
520      getClassProperties(astNode, mCurrentExportedPropertySet);
521      return;
522    }
523
524    if (isVariableStatement(astNode)) {
525      astNode.declarationList.forEachChild((child) => {
526        if (isVariableDeclaration(child) && !mCurrentExportNameSet.has(child.name.getText())) {
527          mCurrentExportNameSet.add(child.name.getText());
528          mCurrentExportedPropertySet.add(child.name.getText());
529        }
530      });
531
532      return;
533    }
534
535    forEachChild(astNode, node => visitProjectExport(node, isRemoteHarFile));
536  };
537
538  function handleExportAssignment(astNode): void {
539    // let xxx; export default xxx = a;
540    if (isBinaryExpression(astNode.expression)) {
541      if (isObjectLiteralExpression(astNode.expression.right)) {
542        getObjectProperties(astNode.expression.right, mCurrentExportedPropertySet);
543        return;
544      }
545
546      if (isClassExpression(astNode.expression.right)) {
547        getClassProperties(astNode.expression.right, mCurrentExportedPropertySet);
548      }
549
550      return;
551    }
552
553    // export = xxx; The xxx here can't be obfuscated
554    // export default yyy; The yyy here can be obfuscated
555    if (isIdentifier(astNode.expression)) {
556      if (!mCurrentExportNameSet.has(astNode.expression.getText())) {
557        mCurrentExportNameSet.add(astNode.expression.getText());
558        mCurrentExportedPropertySet.add(astNode.expression.getText());
559      }
560      return;
561    }
562
563    if (isObjectLiteralExpression(astNode.expression)) {
564      getObjectProperties(astNode.expression, mCurrentExportedPropertySet);
565    }
566  }
567
568  function handleExportDeclaration(astNode: ExportDeclaration, isRemoteHarFile: boolean): void {
569    if (astNode.exportClause) {
570      if (astNode.exportClause.kind === SyntaxKind.NamedExports) {
571        astNode.exportClause.forEachChild((child) => {
572          if (!isExportSpecifier(child)) {
573            return;
574          }
575
576          if (child.propertyName) {
577            let originalName = child.propertyName.getText();
578            if (isRemoteHarFile || astNode.moduleSpecifier) {
579              // For the first condition, this ensures that for remoteHar files,
580              // originalName is still collected into mCurrentExportNameSet to maintain compatibility.
581              // NOTE: This specification needs to be revised to determine whether to add originalName
582              // to mCurrentExportNameSet should be independent of whether it is in a remoteHar file.
583
584              // The second condition indicates that for `export {A as B} from './filePath'` statements,
585              // the original name (A) needs to be added to the export whitelist.
586              mCurrentExportNameSet.add(originalName);
587            } else {
588              /**
589               * In project source code:
590               * class A {
591               *   prop1 = 1;
592               *   prop2 = 2;
593               * }
594               * export {A as B}; // collect A to ensure we can collect prop1 and prop2
595               */
596              exportOriginalNameSet.add(originalName);
597            }
598          }
599
600          let exportName = child.name.getText();
601          mCurrentExportedPropertySet.add(exportName);
602          mCurrentExportNameSet.add(exportName);
603        });
604      }
605
606      if (astNode.exportClause.kind === SyntaxKind.NamespaceExport) {
607        mCurrentExportedPropertySet.add(astNode.exportClause.name.getText());
608        return;
609      }
610    }
611  }
612
613  /**
614   * extract the class, enum, and object properties of the export in the project before obfuscation
615   * class A{};
616   * export = A; need to be considered
617   * export = namespace;
618   * This statement also needs to determine whether there is an export in the namespace, and namespaces are also allowed in the namespace
619   * @param astNode
620   */
621  const visitProjectNode = function (astNode): void {
622    const currentPropsSet: Set<string> = new Set();
623    let nodeName: string | undefined = astNode.name?.text;
624    if ((isClassDeclaration(astNode) || isStructDeclaration(astNode))) {
625      getClassProperties(astNode, currentPropsSet);
626    } else if (isEnumDeclaration(astNode)) { // collect export enum structure properties
627      getEnumProperties(astNode, currentPropsSet);
628    } else if (isVariableDeclaration(astNode)) {
629      if (astNode.initializer) {
630        if (isObjectLiteralExpression(astNode.initializer)) {
631          getObjectProperties(astNode.initializer, currentPropsSet);
632        } else if (isClassExpression(astNode.initializer)) {
633          getClassProperties(astNode.initializer, currentPropsSet);
634        }
635      }
636      nodeName = astNode.name?.getText();
637    } else if (isInterfaceDeclaration(astNode)) {
638      getInterfaceProperties(astNode, currentPropsSet);
639    } else if (isTypeAliasDeclaration(astNode)) {
640      getTypeAliasProperties(astNode, currentPropsSet);
641    } else if (isElementAccessExpression(astNode)) {
642      getElementAccessExpressionProperties(astNode);
643    } else if (isIndexedAccessTypeNode(astNode)) {
644      getIndexedAccessTypeProperties(astNode);
645    } else if (isObjectLiteralExpression(astNode)) {
646      getObjectProperties(astNode, currentPropsSet);
647    } else if (isClassExpression(astNode)) {
648      getClassProperties(astNode, currentPropsSet);
649    }
650
651    addPropWhiteList(nodeName, astNode, currentPropsSet);
652
653    forEachChild(astNode, visitProjectNode);
654  };
655
656  function addPropWhiteList(nodeName: string | undefined, astNode: Node, currentPropsSet: Set<string>): void {
657    if (nodeName && (mCurrentExportNameSet.has(nodeName) || exportOriginalNameSet.has(nodeName))) {
658      addElement(currentPropsSet);
659    }
660
661    if (scanProjectConfig.isHarCompiled && scanProjectConfig.mPropertyObfuscation && isEnumDeclaration(astNode)) {
662      addEnumElement(currentPropsSet);
663    }
664  }
665
666  function addElement(currentPropsSet: Set<string>): void {
667    currentPropsSet.forEach((element: string) => {
668      mCurrentExportedPropertySet.add(element);
669    });
670  }
671
672  function addEnumElement(currentPropsSet: Set<string>): void {
673    currentPropsSet.forEach((element: string) => {
674      enumPropsSet.add(element);
675      projectWhiteListManager?.fileWhiteListInfo?.fileKeepInfo.enumProperties.add(element);
676    });
677  }
678  /**
679   * parse file to api list and save to json object
680   * @param fileName file name of api file
681   * @param apiType
682   * @private
683   */
684  const parseFile = function (fileName: string, apiType: ApiType): void {
685    if (!FileUtils.isReadableFile(fileName) || !isParsableFile(fileName)) {
686      return;
687    }
688
689    projectWhiteListManager?.setCurrentCollector(fileName);
690
691    const sourceFile: SourceFile = createSourceFile(fileName, fs.readFileSync(fileName).toString(), ScriptTarget.ES2015, true);
692    mCurrentExportedPropertySet.clear();
693
694    collectWhiteListByApiType(sourceFile, apiType, fileName);
695
696    // collect names marked with '// @KeepSymbol' or '// @KeepAsConsumer', only support .ts/.ets
697    if (shouldCollectAtKeep(fileName)) {
698      collectAndAddAtKeepNames(sourceFile);
699    }
700
701    // collect origin source file white lists
702    if (shouldCollectFileWhiteLists(apiType)) {
703      collectFileWhiteLists();
704    }
705
706    // collect export names.
707    mCurrentExportNameSet.forEach(item => mExportNames.add(item));
708    mCurrentExportNameSet.clear();
709    // collect export names and properties.
710    mCurrentExportedPropertySet.forEach(item => mPropertySet.add(item));
711    mCurrentExportedPropertySet.clear();
712    exportOriginalNameSet.clear();
713  };
714
715  function shouldCollectAtKeep(fileName: string): boolean {
716    return scanProjectConfig.mEnableAtKeep &&
717      !(fileName.endsWith(Extension.Dts) || fileName.endsWith(Extension.Dets)) &&
718      (fileName.endsWith(Extension.Ts) || fileName.endsWith(Extension.Ets));
719  }
720
721  function collectAndAddAtKeepNames(sourceFile: SourceFile): void {
722    clearAtKeepTemp();
723    collectNamesWithAtKeep(sourceFile, sourceFile);
724    addToSet(AtKeepCollections.keepSymbol.globalNames, keepSymbolTemp.globalNames);
725    addToSet(AtKeepCollections.keepSymbol.propertyNames, keepSymbolTemp.propertyNames);
726    addToSet(AtKeepCollections.keepAsConsumer.globalNames, keepAsConsumerTemp.globalNames);
727    addToSet(AtKeepCollections.keepAsConsumer.propertyNames, keepAsConsumerTemp.propertyNames);
728  }
729
730  function shouldCollectFileWhiteLists(apiType: ApiType): boolean {
731    return apiType === ApiType.PROJECT || apiType === ApiType.CONSTRUCTOR_PROPERTY;
732  }
733
734  function collectFileWhiteLists(): void {
735    const fileWhiteLists: FileWhiteList | undefined = projectWhiteListManager?.fileWhiteListInfo;
736    if (!fileWhiteLists) {
737      return;
738    }
739
740    if (scanProjectConfig.mPropertyObfuscation) {
741      addToSet(fileWhiteLists.fileKeepInfo.exported.propertyNames, mCurrentExportedPropertySet);
742      if (!scanProjectConfig.mKeepStringProperty) {
743        fileWhiteLists.fileKeepInfo.stringProperties.clear();
744      }
745    } else {
746      fileWhiteLists.fileKeepInfo.structProperties.clear();
747      fileWhiteLists.fileKeepInfo.stringProperties.clear();
748      fileWhiteLists.fileKeepInfo.enumProperties.clear();
749    }
750    if (scanProjectConfig.mExportObfuscation) {
751      addToSet(fileWhiteLists.fileKeepInfo.exported.globalNames, mCurrentExportNameSet);
752    }
753    if (scanProjectConfig.mEnableAtKeep) {
754      addToSet(fileWhiteLists.fileKeepInfo.keepSymbol.globalNames, keepSymbolTemp.globalNames);
755      addToSet(fileWhiteLists.fileKeepInfo.keepSymbol.propertyNames, keepSymbolTemp.propertyNames);
756      addToSet(fileWhiteLists.fileKeepInfo.keepAsConsumer.globalNames, keepAsConsumerTemp.globalNames);
757      addToSet(fileWhiteLists.fileKeepInfo.keepAsConsumer.propertyNames, keepAsConsumerTemp.propertyNames);
758    }
759  }
760
761  function collectWhiteListByApiType(sourceFile: SourceFile, apiType: ApiType, fileName: string): void {
762    // get export name list
763    switch (apiType) {
764      case ApiType.COMPONENT:
765        forEachChild(sourceFile, node => visitChildNode(node, true));
766        break;
767      case ApiType.KEEP_DTS:
768        forEachChild(sourceFile, visitChildNode);
769        break;
770      case ApiType.API:
771        mCurrentExportNameSet.clear();
772        forEachChild(sourceFile, node => visitExport(node, true));
773        mCurrentExportNameSet.forEach(item => mSystemExportSet.add(item));
774
775        forEachChild(sourceFile, visitPropertyAndNameForSdk);
776        mCurrentExportNameSet.clear();
777        break;
778      case ApiType.PROJECT:
779        mCurrentExportNameSet.clear();
780        if (fileName.endsWith('.d.ts') || fileName.endsWith('.d.ets')) {
781          forEachChild(sourceFile, visitChildNode);
782        }
783
784        let isRemoteHarFile = isRemoteHar(fileName);
785        forEachChild(sourceFile, node => visitProjectExport(node, isRemoteHarFile));
786        forEachChild(sourceFile, visitProjectNode);
787        collectEnumMembersOfFile(sourceFile);
788        mCurrentExportedPropertySet = handleWhiteListWhenExportObfs(fileName, mCurrentExportedPropertySet);
789        mCurrentExportNameSet = handleWhiteListWhenExportObfs(fileName, mCurrentExportNameSet);
790        break;
791      case ApiType.CONSTRUCTOR_PROPERTY:
792        forEachChild(sourceFile, visitNodeForConstructorProperty);
793        collectEnumMembersOfFile(sourceFile);
794        break;
795      default:
796        break;
797    }
798  }
799
800  function handleWhiteListWhenExportObfs(fileName: string, collectedExportNamesAndProperties: Set<string>): Set<string> {
801    // If mExportObfuscation is not enabled, collect the export names and their properties into the whitelist.
802    if (!scanProjectConfig.mExportObfuscation) {
803      return collectedExportNamesAndProperties;
804    }
805    // If the current file is a keep file or its dependent file, collect the export names and their properties into the whitelist.
806    if (scanProjectConfig.mkeepFilesAndDependencies?.has(fileName)) {
807      return collectedExportNamesAndProperties;
808    }
809    // If it is a project source code file, the names and their properties of the export will not be collected.
810    if (!isRemoteHar(fileName)) {
811      collectedExportNamesAndProperties.clear();
812      return collectedExportNamesAndProperties;
813    }
814    // If it is a third-party library file.
815    return collectedExportNamesAndProperties;
816  }
817
818  const projectExtensions: string[] = ['.ets', '.ts', '.js'];
819  const projectDependencyExtensions: string[] = ['.d.ets', '.d.ts', '.ets', '.ts', '.js'];
820  const resolvedModules = new Set();
821
822  function tryGetPackageID(filePath: string): string {
823    const ohPackageJsonPath = path.join(filePath, 'oh-package.json5');
824    let packgeNameAndVersion = '';
825    if (fs.existsSync(ohPackageJsonPath)) {
826      const ohPackageContent = json5.parse(fs.readFileSync(ohPackageJsonPath, 'utf-8'));
827      packgeNameAndVersion = ohPackageContent.name + ohPackageContent.version;
828    }
829    return packgeNameAndVersion;
830  }
831
832  function traverseFilesInDir(apiPath: string, apiType: ApiType): void {
833    let fileNames: string[] = fs.readdirSync(apiPath);
834    for (let fileName of fileNames) {
835      let filePath: string = path.join(apiPath, fileName);
836      try {
837        fs.accessSync(filePath, fs.constants.R_OK);
838      } catch (err) {
839        continue;
840      }
841      if (fs.statSync(filePath).isDirectory()) {
842        const packgeNameAndVersion = tryGetPackageID(filePath);
843        if (resolvedModules.has(packgeNameAndVersion)) {
844          continue;
845        }
846        traverseApiFiles(filePath, apiType);
847        packgeNameAndVersion.length > 0 && resolvedModules.add(packgeNameAndVersion);
848        continue;
849      }
850      const suffix: string = path.extname(filePath);
851      if ((apiType !== ApiType.PROJECT) && !projectDependencyExtensions.includes(suffix)) {
852        continue;
853      }
854
855      if (apiType === ApiType.PROJECT && !projectExtensions.includes(suffix)) {
856        continue;
857      }
858      parseFile(filePath, apiType);
859    }
860  }
861
862  /**
863   * traverse files of  api directory
864   * @param apiPath api directory path
865   * @param apiType
866   * @private
867   */
868  export const traverseApiFiles = function (apiPath: string, apiType: ApiType): void {
869    if (fs.statSync(apiPath).isDirectory()) {
870      traverseFilesInDir(apiPath, apiType);
871    } else {
872      parseFile(apiPath, apiType);
873    }
874  };
875
876  /**
877   * desc: parse openHarmony sdk to get api list
878   * @param version version of api, e.g. version 5.0.1.0 for api 9
879   * @param sdkPath sdk real path of openHarmony
880   * @param isEts true for ets, false for js
881   * @param outputDir: sdk api output directory
882   */
883  export function parseOhSdk(sdkPath: string, version: string, isEts: boolean, outputDir: string): void {
884    mPropertySet.clear();
885
886    // visit api directory
887    const apiPath: string = path.join(sdkPath, (isEts ? 'ets' : 'js'), version, 'api');
888    traverseApiFiles(apiPath, ApiType.API);
889
890    // visit component directory if ets
891    if (isEts) {
892      const componentPath: string = path.join(sdkPath, 'ets', version, 'component');
893      traverseApiFiles(componentPath, ApiType.COMPONENT);
894    }
895
896    // visit the UI conversion API
897    const uiConversionPath: string = path.join(sdkPath, (isEts ? 'ets' : 'js'), version,
898      'build-tools', 'ets-loader', 'lib', 'pre_define.js');
899    extractStringsFromFile(uiConversionPath);
900
901    const reservedProperties: string[] = [...mPropertySet.values()];
902    mPropertySet.clear();
903
904    writeToFile(reservedProperties, path.join(outputDir, 'propertiesReserved.json'));
905  }
906
907  export function extractStringsFromFile(filePath: string): void {
908    let collections: string[] = [];
909    const fileContent = fs.readFileSync(filePath, 'utf-8');
910    const regex = /"([^"]*)"/g;
911    const matches = fileContent.match(regex);
912
913    if (matches) {
914      collections = matches.map(match => match.slice(1, -1));
915    }
916
917    collections.forEach(name => mPropertySet.add(name));
918  }
919
920  /**
921   * save api json object to file
922   * @private
923   */
924  export function writeToFile(reservedProperties: string[], outputPath: string): void {
925    let str: string = JSON.stringify(reservedProperties, null, '\t');
926    fs.writeFileSync(outputPath, str);
927  }
928
929  export function isRemoteHar(filePath: string): boolean {
930    const realPath: string = sys.realpath(filePath);
931    return isInOhModuleFile(realPath);
932  }
933
934  export function isInOhModuleFile(filePath: string): boolean {
935    return filePath.indexOf('/oh_modules/') !== -1 || filePath.indexOf('\\oh_modules\\') !== -1;
936  }
937
938  export function isParsableFile(path: string): boolean {
939    return supportedParsingExtension.some(extension => path.endsWith(extension));
940  }
941
942  /**
943  * parse common project or file to extract exported api list
944  * @return reserved api names
945  */
946  export function parseFileByPaths(projectPaths: Set<string>, scanningApiType: ApiType):
947    {reservedExportPropertyAndName: Set<string> | undefined; reservedExportNames: Set<string> | undefined} {
948    mPropertySet.clear();
949    mExportNames.clear();
950    projectPaths.forEach(path => {
951      parseFile(path, scanningApiType);
952    });
953    let reservedExportPropertyAndName: Set<string>;
954    let reservedExportNames: Set<string>;
955    if (scanProjectConfig.mPropertyObfuscation) {
956      reservedExportPropertyAndName = new Set(mPropertySet);
957    }
958    if (scanProjectConfig.mExportObfuscation) {
959      reservedExportNames = new Set(mExportNames);
960    }
961    mPropertySet.clear();
962    mExportNames.clear();
963    return {
964      reservedExportPropertyAndName: reservedExportPropertyAndName,
965      reservedExportNames: reservedExportNames
966    };
967  }
968
969  /**
970   * Collect all property names in the AST.
971   * @param astNode Nodes of the AST.
972   */
973  function collectPropertyNames(astNode: Node): void {
974    visitElementsWithProperties(astNode);
975  }
976
977  /**
978   * Visit elements that can contain properties.
979   * @param node The current AST node.
980   */
981  function visitElementsWithProperties(node: Node): void {
982    switch (node.kind) {
983      case SyntaxKind.ClassDeclaration:
984        forEachChild(node, visitClass);
985        break;
986      case SyntaxKind.InterfaceDeclaration:
987      case SyntaxKind.TypeLiteral:
988        forEachChild(node, visitInterfaceOrType);
989        break;
990      case SyntaxKind.EnumDeclaration:
991        forEachChild(node, visitEnum);
992        break;
993      case SyntaxKind.ObjectLiteralExpression:
994        forEachChild(node, visitObjectLiteral);
995        break;
996      case SyntaxKind.ModuleDeclaration:
997        forEachChild(node, visitModule);
998        break;
999    }
1000    forEachChild(node, visitElementsWithProperties);
1001  }
1002
1003  function visitClass(node: Node): void {
1004    if (isPropertyDeclaration(node) || isMethodDeclaration(node)) {
1005      if (isIdentifier(node.name)) {
1006        mCurrentExportedPropertySet.add(node.name.text);
1007      }
1008    }
1009    forEachChild(node, visitClass);
1010  }
1011
1012  function visitInterfaceOrType(node: Node): void {
1013    if (isPropertySignature(node) || isMethodSignature(node)) {
1014      if (isIdentifier(node.name)) {
1015        mCurrentExportedPropertySet.add(node.name.text);
1016      }
1017    }
1018    forEachChild(node, visitInterfaceOrType);
1019  }
1020
1021  function visitEnum(node: Node): void {
1022    if (isEnumMember(node) && isIdentifier(node.name)) {
1023      mCurrentExportedPropertySet.add(node.name.text);
1024    }
1025  }
1026
1027  function visitObjectLiteral(node: Node): void {
1028    if (isPropertyAssignment(node)) {
1029      if (isIdentifier(node.name)) {
1030        mCurrentExportedPropertySet.add(node.name.text);
1031      }
1032    }
1033    forEachChild(node, visitObjectLiteral);
1034  }
1035
1036  function visitModule(node: Node): void {
1037    forEachChild(node, visitElementsWithProperties);
1038  }
1039
1040  function collectNodeName(name: string): void {
1041    mCurrentExportNameSet.add(name);
1042    mCurrentExportedPropertySet.add(name);
1043  }
1044
1045  function containsIdentifier(node: Node, found: { value: boolean }): void {
1046    if (found.value) {
1047      return;
1048    }
1049    if (isIdentifier(node)) {
1050      found.value = true;
1051      return;
1052    }
1053    forEachChild(node, childNode => {
1054      containsIdentifier(childNode, found);
1055    });
1056  }
1057
1058  function shouldCollectEnum(node: EnumDeclaration): boolean {
1059    const members = node.members;
1060    for (const member of members) {
1061      if (isEnumMember(member) && member.initializer) {
1062        const initializer = member.initializer;
1063        const found = { value: false };
1064        containsIdentifier(initializer, found);
1065        if (found.value) {
1066          return true;
1067        }
1068      }
1069    }
1070    return false;
1071  }
1072
1073  function collectEnumMember(node: Node): void {
1074    if (isEnumMember(node) && isIdentifier(node.name)) {
1075      mEnumMemberSet.add(node.name.text);
1076      projectWhiteListManager?.fileWhiteListInfo?.fileReservedInfo.enumProperties.add(node.name.text);
1077    }
1078  }
1079
1080  /**
1081   * Visit and collect enum members
1082   * @param node The current AST node.
1083   */
1084  function visitEnumMembers(node: Node): void {
1085    if (isEnumDeclaration(node)) {
1086      if (!shouldCollectEnum(node)) {
1087        return;
1088      }
1089      for (const member of node.members) {
1090        collectEnumMember(member);
1091      }
1092      return;
1093    }
1094    forEachChild(node, visitEnumMembers);
1095  }
1096
1097  /**
1098   * Visit and collect enum members of non-js file
1099   * @param sourceFile The sourceFile to collect.
1100   */
1101  function collectEnumMembersOfFile(sourceFile: SourceFile): void {
1102    if (sourceFile.fileName.endsWith(Extension.Js)) {
1103      return;
1104    }
1105    forEachChild(sourceFile, visitEnumMembers);
1106  }
1107
1108  function collectNamesWithAtKeep(node: Node, sourceFile: SourceFile): void {
1109    switch (node.kind) {
1110      case SyntaxKind.ClassDeclaration:
1111        collectClassDeclaration(node as ClassDeclaration, sourceFile);
1112        break;
1113      case SyntaxKind.InterfaceDeclaration:
1114        collectInterfaceDeclaration(node as InterfaceDeclaration, sourceFile);
1115        break;
1116      case SyntaxKind.EnumDeclaration:
1117        collectEnumDeclaration(node as EnumDeclaration, sourceFile);
1118        break;
1119      case SyntaxKind.FunctionDeclaration:
1120        collectFunctionDeclaration(node as FunctionDeclaration, sourceFile);
1121        break;
1122      case SyntaxKind.VariableStatement:
1123        collectVariableDeclararion(node as VariableStatement, sourceFile);
1124        break;
1125      case SyntaxKind.ModuleDeclaration:
1126        collectModuleDeclaration(node as ModuleDeclaration, sourceFile);
1127        break;
1128    }
1129    forEachChild(node, child => collectNamesWithAtKeep(child, sourceFile));
1130  }
1131
1132  function collectClassDeclaration(node: ClassDeclaration, sourceFile: SourceFile): void {
1133    const atKeepType: AtKeepType = getAtKeepType(node, sourceFile);
1134    const isToplevel: boolean = isSourceFile(node.parent);
1135    const isExported: boolean = hasExportModifier(node);
1136
1137    if (atKeepType === AtKeepType.KeepAsConsumer) {
1138      collectToplevelOrExportedNames(node, isToplevel, isExported, atKeepType);
1139      collectClassDeclarationMembers(node, atKeepType);
1140    } else if (atKeepType === AtKeepType.KeepSymbol) {
1141      collectToplevelOrExportedNames(node, isToplevel, isExported, atKeepType);
1142      collectClassDeclarationMembers(node, atKeepType);
1143      scanAndCollectClassDeclarationMembers(node, sourceFile, isToplevel, isExported);
1144    } else { // atKeepType === AtKeepType.None
1145      scanAndCollectClassDeclarationMembers(node, sourceFile, isToplevel, isExported);
1146    }
1147  }
1148
1149  function collectClassDeclarationMembers(node: ClassDeclaration, atKeepType: AtKeepType): void {
1150    for (const member of node.members) {
1151      collectClassMemberNames(member, atKeepType);
1152      collectParameterPropertyNames(member, atKeepType);
1153    }
1154  }
1155
1156  function collectClassMemberNames(member: ClassElement, atKeepType: AtKeepType): void {
1157    if (isPropertyDeclaration(member) || isMethodDeclaration(member) || isGetAccessor(member) || isSetAccessor(member)) {
1158      if (isIdentifier(member.name)) {
1159        collectAtKeepNamesByType(member.name.text, atKeepType, WhiteListType.PropertyName);
1160      }
1161    }
1162  }
1163
1164  function collectParameterPropertyNames(member: ClassElement, atKeepType: AtKeepType): void {
1165    if (isConstructorDeclaration(member)) {
1166      member.parameters.forEach((param) => {
1167        if (isParameterPropertyDeclaration(param, member) && isIdentifier(param.name)) {
1168          collectAtKeepNamesByType(param.name.text, atKeepType, WhiteListType.PropertyName);
1169        }
1170      });
1171    }
1172  }
1173
1174  function scanAndCollectClassDeclarationMembers(node: ClassDeclaration, sourceFile: SourceFile, isToplevel: boolean, isExported: boolean): void {
1175    let shouldKeepClassName: boolean = false;
1176    let atKeepTypeOfClass: AtKeepType = AtKeepType.KeepSymbol;
1177    for (const member of node.members) {
1178      const atKeepType: AtKeepType = getAtKeepType(member, sourceFile);
1179      if (atKeepType === AtKeepType.None) {
1180        continue;
1181      }
1182      if (atKeepType === AtKeepType.KeepAsConsumer) {
1183        atKeepTypeOfClass = AtKeepType.KeepAsConsumer;
1184      }
1185      if (isPropertyDeclaration(member) || isMethodDeclaration(member) || isGetAccessor(member) || isSetAccessor(member)) {
1186        if (isIdentifier(member.name)) {
1187          shouldKeepClassName = true;
1188          collectAtKeepNamesByType(member.name.text, atKeepType, WhiteListType.PropertyName);
1189        }
1190      }
1191      if (isConstructorDeclaration(member)) {
1192        shouldKeepClassName = true;
1193      }
1194    }
1195    if (shouldKeepClassName) {
1196      collectToplevelOrExportedNames(node, isToplevel, isExported, atKeepTypeOfClass);
1197    }
1198  }
1199
1200  function collectInterfaceDeclaration(node: InterfaceDeclaration, sourceFile: SourceFile): void {
1201    const atKeepType: AtKeepType = getAtKeepType(node, sourceFile);
1202    const isToplevel: boolean = isSourceFile(node.parent);
1203    const isExported: boolean = hasExportModifier(node);
1204
1205    if (atKeepType === AtKeepType.KeepAsConsumer) {
1206      collectToplevelOrExportedNames(node, isToplevel, isExported, atKeepType);
1207      collectInterfaceDeclarationMembers(node, atKeepType);
1208    } else if (atKeepType === AtKeepType.KeepSymbol) {
1209      collectToplevelOrExportedNames(node, isToplevel, isExported, atKeepType);
1210      collectInterfaceDeclarationMembers(node, atKeepType);
1211      scanAndCollectInterfaceDeclarationMembers(node, sourceFile, isToplevel, isExported);
1212    } else { // atKeepType === AtKeepType.None
1213      scanAndCollectInterfaceDeclarationMembers(node, sourceFile, isToplevel, isExported);
1214    }
1215  }
1216
1217  function collectInterfaceDeclarationMembers(node: InterfaceDeclaration, atKeepType: AtKeepType): void {
1218    for (const member of node.members) {
1219      if (isPropertySignature(member) || isMethodSignature(member)) {
1220        if (isIdentifier(member.name)) {
1221          collectAtKeepNamesByType(member.name.text, atKeepType, WhiteListType.PropertyName);
1222        }
1223      }
1224    }
1225  }
1226
1227  function scanAndCollectInterfaceDeclarationMembers(node: InterfaceDeclaration, sourceFile: SourceFile, isToplevel: boolean, isExported: boolean): void {
1228    let shouldKeepInterfaceName: boolean = false;
1229    let atKeepTypeOfInterface: AtKeepType = AtKeepType.KeepSymbol;
1230    for (const member of node.members) {
1231      const atKeepType: AtKeepType = getAtKeepType(member, sourceFile);
1232      if (atKeepType === AtKeepType.None) {
1233        continue;
1234      }
1235      if (atKeepType === AtKeepType.KeepAsConsumer) {
1236        atKeepTypeOfInterface = AtKeepType.KeepAsConsumer;
1237      }
1238      if (isPropertySignature(member) || isMethodSignature(member)) {
1239        if (isIdentifier(member.name)) {
1240          shouldKeepInterfaceName = true;
1241          collectAtKeepNamesByType(member.name.text, atKeepType, WhiteListType.PropertyName);
1242        }
1243      }
1244    }
1245    if (shouldKeepInterfaceName) {
1246      collectToplevelOrExportedNames(node, isToplevel, isExported, atKeepTypeOfInterface);
1247    }
1248  }
1249
1250  function collectEnumDeclaration(node: EnumDeclaration, sourceFile: SourceFile): void {
1251    const atKeepType: AtKeepType = getAtKeepType(node, sourceFile);
1252    const isToplevel: boolean = isSourceFile(node.parent);
1253    const isExported: boolean = hasExportModifier(node);
1254
1255    if (atKeepType === AtKeepType.KeepAsConsumer) {
1256      collectToplevelOrExportedNames(node, isToplevel, isExported, atKeepType);
1257      collectEnumDeclarationMembers(node, atKeepType);
1258    } else if (atKeepType === AtKeepType.KeepSymbol) {
1259      collectToplevelOrExportedNames(node, isToplevel, isExported, atKeepType);
1260      collectEnumDeclarationMembers(node, atKeepType);
1261      scanAndCollectEnumDeclarationMembers(node, sourceFile, isToplevel, isExported);
1262    } else { // atKeepType === AtKeepType.None
1263      scanAndCollectEnumDeclarationMembers(node, sourceFile, isToplevel, isExported);
1264    }
1265  }
1266
1267  function collectEnumDeclarationMembers(node: EnumDeclaration, atKeepType: AtKeepType): void {
1268    for (const member of node.members) {
1269      if (isEnumMember(member)) {
1270        if (isIdentifier(member.name)) {
1271          collectAtKeepNamesByType(member.name.text, atKeepType, WhiteListType.PropertyName);
1272        }
1273      }
1274    }
1275  }
1276
1277  function scanAndCollectEnumDeclarationMembers(node: EnumDeclaration, sourceFile: SourceFile, isToplevel: boolean, isExported: boolean): void {
1278    let shouldKeepEnumName: boolean = false;
1279    let atKeepTypeOfEnum: AtKeepType = AtKeepType.KeepSymbol;
1280    for (const member of node.members) {
1281      const atKeepType: AtKeepType = getAtKeepType(member, sourceFile);
1282      if (atKeepType === AtKeepType.None) {
1283        continue;
1284      }
1285      if (atKeepType === AtKeepType.KeepAsConsumer) {
1286        atKeepTypeOfEnum = AtKeepType.KeepAsConsumer;
1287      }
1288      if (isEnumMember(member)) {
1289        if (isIdentifier(member.name)) {
1290          shouldKeepEnumName = true;
1291          collectAtKeepNamesByType(member.name.text, atKeepType, WhiteListType.PropertyName);
1292        }
1293      }
1294    }
1295    if (shouldKeepEnumName) {
1296      collectToplevelOrExportedNames(node, isToplevel, isExported, atKeepTypeOfEnum);
1297    }
1298  }
1299
1300  function collectFunctionDeclaration(node: FunctionDeclaration, sourceFile: SourceFile): void {
1301    const atKeepType: AtKeepType = getAtKeepType(node, sourceFile);
1302    const isToplevel: boolean = isSourceFile(node.parent);
1303    const isExported: boolean = hasExportModifier(node);
1304    collectToplevelOrExportedNames(node, isToplevel, isExported, atKeepType);
1305  }
1306
1307  function collectVariableDeclararion(node: VariableStatement, sourceFile: SourceFile): void {
1308    const atKeepType: AtKeepType = getAtKeepType(node, sourceFile);
1309    const isToplevel: boolean = isSourceFile(node.parent);
1310    const isExported: boolean = hasExportModifier(node);
1311    node.declarationList.forEachChild((child) => {
1312      if (isVariableDeclaration(child)) {
1313        collectToplevelOrExportedNames(child, isToplevel, isExported, atKeepType);
1314      }
1315    });
1316  }
1317
1318  function collectModuleDeclaration(node: ModuleDeclaration, sourceFile: SourceFile, atKeepTypeOuter?: AtKeepType): void {
1319    let atKeepType: AtKeepType;
1320    if (atKeepTypeOuter) {
1321      atKeepType = atKeepTypeOuter;
1322    } else {
1323      atKeepType = getAtKeepType(node, sourceFile);
1324    }
1325    const isToplevel: boolean = isSourceFile(node.parent);
1326    const isExported: boolean = hasExportModifier(node);
1327    collectToplevelOrExportedNames(node, isToplevel, isExported, atKeepType);
1328
1329    if (atKeepType !== AtKeepType.None && isModuleBlock(node.body)) {
1330      node.body.statements.forEach((child) => {
1331        collectModuleChild(child, atKeepType, sourceFile);
1332      });
1333    }
1334  }
1335
1336  function collectModuleChild(child: Node, atKeepType: AtKeepType, sourceFile: SourceFile): void {
1337    const isExportedChild: boolean = hasExportModifier(child);
1338    if (!isExportedChild) {
1339      return;
1340    }
1341
1342    switch (child.kind) {
1343      case SyntaxKind.ClassDeclaration:
1344      case SyntaxKind.InterfaceDeclaration:
1345      case SyntaxKind.EnumDeclaration:
1346      case SyntaxKind.FunctionDeclaration:
1347      case SyntaxKind.TypeAliasDeclaration:
1348        collectToplevelOrExportedNames(child as KeepTargetNode, false, true, atKeepType);
1349        break;
1350      case SyntaxKind.ModuleDeclaration:
1351        collectToplevelOrExportedNames(child as ModuleDeclaration, false, true, atKeepType);
1352        collectModuleDeclaration(child as ModuleDeclaration, sourceFile, atKeepType);
1353        break;
1354      case SyntaxKind.VariableStatement:
1355        (child as VariableStatement).declarationList.forEachChild((variableDeclaration) => {
1356          if (isVariableDeclaration(variableDeclaration)) {
1357            collectToplevelOrExportedNames(variableDeclaration, false, true, atKeepType);
1358          }
1359        });
1360        break;
1361    }
1362  }
1363
1364  function getAtKeepType(node: Node, sourceFile: SourceFile): AtKeepType {
1365    const ranges: CommentRange[] | undefined = getLeadingCommentRangesOfNode(node, sourceFile);
1366    let atKeepType: AtKeepType = AtKeepType.None;
1367    if (!ranges?.length) {
1368      return atKeepType;
1369    }
1370    for (const range of ranges) {
1371      if (range.kind !== SyntaxKind.SingleLineCommentTrivia) {
1372        continue;
1373      }
1374      const comment: string = sourceFile.text.slice(range.pos, range.end).replace(/\s+/g, '');
1375      if (comment === KEEP_AS_CONSUMER) {
1376        atKeepType = AtKeepType.KeepAsConsumer;
1377        return atKeepType;
1378      }
1379      if (comment === KEEP_SYMBOL) {
1380        atKeepType = AtKeepType.KeepSymbol;
1381      }
1382    }
1383    return atKeepType;
1384  }
1385
1386  function collectToplevelOrExportedNames(node: KeepTargetNode, isToplevel: boolean, isExported: boolean, atKeepType: AtKeepType): void {
1387    if ((!isToplevel && !isExported) || !isIdentifier(node.name)) {
1388      return;
1389    }
1390
1391    collectAtKeepNamesByType(node.name.text, atKeepType, WhiteListType.GlobalName);
1392
1393    if (isExported) {
1394      collectAtKeepNamesByType(node.name.text, atKeepType, WhiteListType.PropertyName);
1395    }
1396  }
1397
1398  function collectAtKeepNamesByType(name: string, atKeepType: AtKeepType, whiteListType: WhiteListType): void {
1399    if (atKeepType === AtKeepType.None) {
1400      return;
1401    }
1402
1403    const targetCollection = atKeepType === AtKeepType.KeepSymbol
1404      ? keepSymbolTemp
1405      : keepAsConsumerTemp;
1406
1407    updateKeepCollection(targetCollection, whiteListType, name);
1408  }
1409
1410  function updateKeepCollection(collector: KeepInfo, whiteListType: WhiteListType, name: string): void {
1411    if (whiteListType === WhiteListType.PropertyName) {
1412      collector.propertyNames.add(name);
1413    } else {
1414      collector.globalNames.add(name);
1415    }
1416  }
1417}
1418