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