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