1/*
2 * Copyright (c) 2024 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 {ListUtil} from '../../../src/utils/ListUtil';
17import {describe, it} from 'mocha';
18import {assert} from 'chai';
19import * as fs from 'fs';
20import sinon from 'sinon';
21import {TypeUtils} from '../../../src/utils/TypeUtils';
22import {
23  Label,
24  Scope,
25  ScopeKind,
26  ScopeManager,
27  createLabel,
28  createScopeManager,
29  isClassScope,
30  isEnumScope,
31  isFunctionScope,
32  isGlobalScope,
33  isInterfaceScope,
34  isObjectLiteralScope
35} from '../../../src/utils/ScopeAnalyzer';
36import { ScriptTarget, SourceFile, createSourceFile, isSourceFile, Symbol, __String, Declaration, JSDocTagInfo,
37   SymbolDisplayPart, SymbolFlags, TypeChecker, LabeledStatement, Identifier, SyntaxKind,
38   isIdentifier,
39   factory,
40   VariableStatement} from 'typescript';
41
42describe('ScopeAnalyzer ut', function () {
43  let sourceFile: SourceFile;
44
45  before('init parameters', function () {
46    const sourceFileContent = `
47    //This is a comment
48    class Demo{
49      constructor(public  title: string, public  content: string, public  mark: number) {
50        this.title = title
51        this.content = content
52        this.mark = mark
53      }
54    }
55    `;
56
57    sourceFile = createSourceFile('sourceFile.ts', sourceFileContent, ScriptTarget.ES2015, true);
58  });
59
60  describe('unit test for isGlobalScope', function () {
61    it('is Global Scope', function () {
62      let curScope = new Scope('curScope', sourceFile, ScopeKind.GLOBAL);
63      assert.isTrue(isGlobalScope(curScope));
64    });
65
66    it('is not Global Scope', function () {
67      let curScope = new Scope('curScope', sourceFile, ScopeKind.MODULE);
68      assert.isFalse(isGlobalScope(curScope));
69    });
70  });
71
72  describe('unit test for isFunctionScope', function () {
73    it('is Function Scope', function () {
74      let curScope = new Scope('curScope', sourceFile, ScopeKind.FUNCTION);
75      assert.isTrue(isFunctionScope(curScope));
76    });
77
78    it('is not Function Scope', function () {
79      let curScope = new Scope('curScope', sourceFile, ScopeKind.MODULE);
80      assert.isFalse(isFunctionScope(curScope));
81    });
82  });
83
84  describe('unit test for isClassScope', function () {
85    it('is Class Scope', function () {
86      let curScope = new Scope('curScope', sourceFile, ScopeKind.CLASS);
87      assert.isTrue(isClassScope(curScope));
88    });
89
90    it('is not Class Scope', function () {
91    let curScope = new Scope('curScope', sourceFile, ScopeKind.MODULE);
92      assert.isFalse(isClassScope(curScope));
93    });
94  });
95
96  describe('unit test for isInterfaceScope', function () {
97    it('is Interface Scope', function () {
98      let curScope = new Scope('curScope', sourceFile, ScopeKind.INTERFACE);
99      assert.isTrue(isInterfaceScope(curScope));
100    });
101
102    it('is not Interface Scope', function () {
103    let curScope = new Scope('curScope', sourceFile, ScopeKind.MODULE);
104      assert.isFalse(isInterfaceScope(curScope));
105    });
106  });
107
108  describe('unit test for isEnumScope', function () {
109    it('is Enum Scope', function () {
110      let curScope = new Scope('curScope', sourceFile, ScopeKind.ENUM);
111      assert.isTrue(isEnumScope(curScope));
112    });
113
114    it('is not Enum Scope', function () {
115    let curScope = new Scope('curScope', sourceFile, ScopeKind.MODULE);
116      assert.isFalse(isEnumScope(curScope));
117    });
118  });
119
120  describe('unit test for isObjectLiteralScope', function () {
121    it('is ObjectLiteral Scope', function () {
122      let curScope = new Scope('curScope', sourceFile, ScopeKind.OBJECT_LITERAL);
123      assert.isTrue(isObjectLiteralScope(curScope));
124    });
125
126    it('is not ObjectLiteral Scope', function () {
127    let curScope = new Scope('curScope', sourceFile , ScopeKind.MODULE);
128      assert.isFalse(isObjectLiteralScope(curScope));
129    });
130  });
131
132  describe('unit test for Scope', function () {
133    describe('constructor', function () {
134      it('init', function () {
135        let curScope = new Scope('curScope', sourceFile, ScopeKind.GLOBAL);
136        assert.equal(curScope.name, 'curScope');
137        assert.equal(curScope.kind, ScopeKind.GLOBAL);
138        assert.equal(curScope.block, sourceFile);
139        assert.equal(curScope.parent, undefined);
140        assert.deepEqual(curScope.children, []);
141        assert.deepEqual(curScope.defs, new Set());
142        assert.deepEqual(curScope.labels, []);
143        assert.deepEqual(curScope.importNames, new Set());
144        assert.deepEqual(curScope.exportNames, new Set());
145        assert.deepEqual(curScope.mangledNames, new Set());
146        assert.equal(curScope.loc, 'curScope');
147      });
148    });
149
150    describe('addChild', function () {
151      it('push', function () {
152        let parentScope = new Scope('parentScope', sourceFile, ScopeKind.FUNCTION);
153        let childScope = new Scope('childScope', sourceFile, ScopeKind.BLOCK, false, parentScope);
154        assert.include(parentScope.children, childScope);
155      });
156    });
157
158    describe('addDefinition', function () {
159      it('should add definition symbol to the current scope', function () {
160        let curScope = new Scope('curScope', sourceFile, ScopeKind.GLOBAL);
161        let symbol: Symbol = Object(Symbol('testSymbol'));
162
163        curScope.addDefinition(symbol, false);
164        assert.isTrue(curScope.defs.has(symbol));
165      });
166
167      it('should mark symbol as obfuscateAsProperty if required', function () {
168        let curScope = new Scope('curScope', sourceFile, ScopeKind.GLOBAL);
169        let symbol: Symbol = Object(Symbol('testSymbol'));
170
171        curScope.addDefinition(symbol, true);
172        assert.isTrue(Reflect.get(symbol, 'obfuscateAsProperty'));
173      });
174    });
175
176    describe('addLabel', function () {
177      it('should add a label to the current scope', function () {
178        let curScope = new Scope('curScope', sourceFile, ScopeKind.GLOBAL);
179        let label: Label = {name: 'testLabel', locInfo: 'locInfo', refs: [], parent: undefined, children: [], scope: curScope};
180
181        curScope.addLabel(label);
182        assert.include(curScope.labels, label);
183      });
184    });
185
186    describe('getSymbolLocation', function () {
187      it('should return the correct location if symbol exists', function () {
188        let curScope = new Scope('curScope', sourceFile, ScopeKind.GLOBAL);
189        let symbol: Symbol = {
190          flags: 0,
191          escapedName: 'testSymbol' as __String,
192          declarations: [],
193          valueDeclaration: undefined,
194          members: undefined,
195          exports: undefined,
196          globalExports: undefined,
197          exportSymbol: undefined,
198          name: 'testSymbol',
199          getFlags: function (): SymbolFlags {
200            throw new Error('Function not implemented.');
201          },
202          getEscapedName: function (): __String {
203            throw new Error('Function not implemented.');
204          },
205          getName: function (): string {
206            throw new Error('Function not implemented.');
207          },
208          getDeclarations: function (): Declaration[] | undefined {
209            throw new Error('Function not implemented.');
210          },
211          getDocumentationComment: function (typeChecker: TypeChecker | undefined): SymbolDisplayPart[] {
212            throw new Error('Function not implemented.');
213          },
214          getJsDocTags: function (checker?: TypeChecker): JSDocTagInfo[] {
215            throw new Error('Function not implemented.');
216          }
217        };
218        curScope.addDefinition(symbol);
219        assert.equal(curScope.getSymbolLocation(symbol), 'testSymbol');
220      });
221
222      it('should return an empty string if symbol does not exist', function () {
223        let curScope = new Scope('curScope', sourceFile, ScopeKind.GLOBAL);
224        let symbol: Symbol = Object(Symbol('nonExistentSymbol'));
225
226        assert.equal(curScope.getSymbolLocation(symbol), '');
227      });
228    });
229
230    describe('getLabelLocation', function () {
231      it('should return the correct location if label exists', function () {
232        let curScope = new Scope('curScope', sourceFile, ScopeKind.GLOBAL);
233        let label: Label = {name: 'testLabel', locInfo: 'locInfo', refs: [], parent: undefined, children: [], scope: curScope};
234
235        curScope.addLabel(label);
236        assert.equal(curScope.getLabelLocation(label), 'testLabel');
237      });
238
239      it('should return an empty string if label does not exist', function () {
240        let curScope = new Scope('curScope', sourceFile, ScopeKind.GLOBAL);
241        let label: Label = {name: 'nonExistentLabel', locInfo: 'locInfo', refs: [], parent: undefined, children: [], scope: curScope};
242
243        assert.equal(curScope.getLabelLocation(label), '');
244      });
245    });
246  });
247
248  describe('unit test for createLabel', function () {
249    const sourceFileContent = `
250    label: console.log('This is a labeled statement');
251    `;
252    sourceFile = createSourceFile('test.ts', sourceFileContent, ScriptTarget.ES2015, true);
253    let labeledStatement: LabeledStatement = sourceFile.statements[0] as LabeledStatement;
254    let curScope = new Scope('curScope', sourceFile, ScopeKind.GLOBAL);
255
256    it('should create a label and add it to the scope', function () {
257      const label = createLabel(labeledStatement, curScope);
258
259      assert.equal(label.name, labeledStatement.label.text);
260      assert.equal(label.locInfo, `$0_${labeledStatement.label.text}`);
261      assert.deepEqual(label.refs, [labeledStatement.label]);
262      assert.equal(label.parent, undefined);
263      assert.deepEqual(label.children, []);
264      assert.equal(label.scope, curScope);
265      assert.include(curScope.labels, label);
266    });
267
268    it('should create a label with a parent and add it to the parent\'s children', function () {
269      const parentLabel: Label = {
270        name: 'parentLabel',
271        locInfo: 'parentLocInfo',
272        refs: [],
273        parent: undefined,
274        children: [],
275        scope: curScope
276      };
277      const label = createLabel(labeledStatement, curScope, parentLabel);
278
279      assert.equal(label.name, labeledStatement.label.text);
280      assert.equal(label.locInfo, `$1_${labeledStatement.label.text}`);
281      assert.deepEqual(label.refs, [labeledStatement.label]);
282      assert.equal(label.parent, parentLabel);
283      assert.deepEqual(label.children, []);
284      assert.equal(label.scope, curScope);
285      assert.include(curScope.labels, label);
286      assert.include(parentLabel.children, label);
287    });
288  });
289
290  describe('unit test for createScopeManager', function () {
291    let scopeManager: ScopeManager;
292    let sourceFile: SourceFile;
293    let checker: TypeChecker;
294
295    function InitScopeManager(newFilePath: string): void {
296      const fileContent = fs.readFileSync(newFilePath, 'utf8');
297      sourceFile = createSourceFile(newFilePath, fileContent, ScriptTarget.ES2015, true);
298      checker = TypeUtils.createChecker(sourceFile);
299      scopeManager = createScopeManager();
300      scopeManager.analyze(sourceFile, checker, false);
301    }
302
303    describe('analyze', function () {
304      describe('analyzeModule', function () {
305        let filePath = 'test/ut/utils/ScopeAnalyzer/analyzeModule.ts';
306        InitScopeManager(filePath);
307        it('getReservedNames', function () {
308          const reservedNames = scopeManager.getReservedNames();
309          assert.strictEqual(reservedNames.size === 0, true);
310        });
311        it('getRootScope', function () {
312          const rootScope = scopeManager.getRootScope();
313          assert.strictEqual(rootScope.fileExportNames?.size !== 0, true);
314        });
315        describe('getScopeOfNode', function () {
316          it('node is not identifier', function () {
317            const node: SourceFile = sourceFile;
318            const scope = scopeManager.getScopeOfNode(node);
319            assert.strictEqual(scope, undefined);
320          });
321
322          it('node with no symbol', function () {
323            const node: Identifier = factory.createIdentifier('noSymbolIdentifier');
324            const scope = scopeManager.getScopeOfNode(node);
325            assert.strictEqual(scope, undefined);
326          });
327
328          it('get the scope of node', function () {
329            const classDeclaration = sourceFile.statements.find(node => node && node.kind === SyntaxKind.ClassDeclaration);
330            if (!classDeclaration) throw new Error('ClassDeclaration not found');
331            const node: Identifier = (classDeclaration as any)?.name;
332            const scope = scopeManager.getScopeOfNode(node);
333            assert.strictEqual(scope?.name, '');
334          });
335
336          it('get no scope of node', function () {
337            const anotherSourceFile = createSourceFile('another.ts', 'let y = 2;', ScriptTarget.ES2015, true);
338            const node = (anotherSourceFile.statements[0] as VariableStatement).declarationList.declarations[0].name;
339            const scope = scopeManager.getScopeOfNode(node);
340            assert.strictEqual(scope, undefined);
341          });
342        });
343      });
344
345      describe('analyzeFunctionLike', function () {
346        let filePath = 'test/ut/utils/ScopeAnalyzer/analyzeFunctionLike.ts';
347        InitScopeManager(filePath);
348        it('getReservedNames', function () {
349          const reservedNames = scopeManager.getReservedNames();
350          assert.strictEqual(reservedNames.size === 0, true);
351        });
352        it('getRootScope', function () {
353          const rootScope = scopeManager.getRootScope();
354          assert.strictEqual(rootScope.fileExportNames?.size !== 0, true);
355        });
356        describe('getScopeOfNode', function () {
357          it('node is not identifier', function () {
358            const node: SourceFile = sourceFile;
359            const scope = scopeManager.getScopeOfNode(node);
360            assert.strictEqual(scope, undefined);
361          });
362
363          it('node with no symbol', function () {
364            const node: Identifier = factory.createIdentifier('noSymbolIdentifier');
365            const scope = scopeManager.getScopeOfNode(node);
366            assert.strictEqual(scope, undefined);
367          });
368
369          it('get the scope of node', function () {
370            const functionDeclaration = sourceFile.statements.find(node => node && node.kind === SyntaxKind.FunctionDeclaration);
371            if (!functionDeclaration) throw new Error('FunctionDeclaration not found');
372            const node: Identifier = (functionDeclaration as any)?.name;
373            const scope = scopeManager.getScopeOfNode(node);
374            assert.strictEqual(scope?.name, '');
375          });
376
377          it('get no scope of node', function () {
378            const anotherSourceFile = createSourceFile('another.ts', 'let y = 2;', ScriptTarget.ES2015, true);
379            const node = (anotherSourceFile.statements[0] as VariableStatement).declarationList.declarations[0].name;
380            const scope = scopeManager.getScopeOfNode(node);
381            assert.strictEqual(scope, undefined);
382          });
383        });
384      });
385
386      describe('analyzeExportNames', function () {
387        let filePath = 'test/ut/utils/ScopeAnalyzer/analyzeExportNames.ts';
388        InitScopeManager(filePath);
389        it('getReservedNames', function () {
390          const reservedNames = scopeManager.getReservedNames();
391          assert.strictEqual(reservedNames.size === 0, true);
392        });
393        it('getRootScope', function () {
394          const rootScope = scopeManager.getRootScope();
395          assert.strictEqual(rootScope.fileExportNames?.size !== 0, true);
396        });
397        describe('getScopeOfNode', function () {
398          it('node is not identifier', function () {
399            const node: SourceFile = sourceFile;
400            const scope = scopeManager.getScopeOfNode(node);
401            assert.strictEqual(scope, undefined);
402          });
403
404          it('node with no symbol', function () {
405            const node: Identifier = factory.createIdentifier('noSymbolIdentifier');
406            const scope = scopeManager.getScopeOfNode(node);
407            assert.strictEqual(scope, undefined);
408          });
409
410          it('get the scope of node', function () {
411            const exportDeclaration = sourceFile.statements.find(node => node && node.kind === SyntaxKind.ExportDeclaration);
412            if (!exportDeclaration) throw new Error('ExportDeclaration not found');
413
414            const exportClause = (exportDeclaration as any).exportClause;
415            if (!exportClause || !exportClause.elements) throw new Error('ExportSpecifier not found');
416
417            const exportSpecifier = exportClause.elements[0];
418            const node: Identifier = exportSpecifier.name;
419
420            const scope = scopeManager.getScopeOfNode(node);
421            assert.strictEqual(scope?.name, '');
422          });
423
424          it('get no scope of node', function () {
425            const anotherSourceFile = createSourceFile('another.ts', 'let y = 2;', ScriptTarget.ES2015, true);
426            const node = (anotherSourceFile.statements[0] as VariableStatement).declarationList.declarations[0].name;
427            const scope = scopeManager.getScopeOfNode(node);
428            assert.strictEqual(scope, undefined);
429          });
430        });
431      });
432
433      describe('analyzeImportEqualsDeclaration', function () {
434        let filePath = 'test/ut/utils/ScopeAnalyzer/analyzeImportEqualsDeclaration.ts';
435        const fileContent = fs.readFileSync(filePath, 'utf8');
436        let sourceFile = createSourceFile(filePath, fileContent, ScriptTarget.ES2015, true);
437        let checker = TypeUtils.createChecker(sourceFile);
438        let scopeManager = createScopeManager();
439        scopeManager.analyze(sourceFile, checker, false);
440        it('getReservedNames', function () {
441          const reservedNames = scopeManager.getReservedNames();
442          assert.strictEqual(reservedNames.size === 0, true);
443        });
444        it('getRootScope', function () {
445          const rootScope = scopeManager.getRootScope();
446          assert.strictEqual(rootScope.fileExportNames?.size === 2, true);
447          assert.strictEqual(rootScope.fileExportNames?.has("a"), true);
448          assert.strictEqual(rootScope.fileExportNames?.has("b"), true);
449        });
450        describe('getScopeOfNode', function () {
451          it('node is not identifier', function () {
452            const node: SourceFile = sourceFile;
453            const scope = scopeManager.getScopeOfNode(node);
454            assert.strictEqual(scope, undefined);
455          });
456
457          it('node with no symbol', function () {
458            const node: Identifier = factory.createIdentifier('noSymbolIdentifier');
459            const scope = scopeManager.getScopeOfNode(node);
460            assert.strictEqual(scope, undefined);
461          });
462
463          it('get the scope of node', function () {
464            const moduleDeclaration = sourceFile.statements.find(node => node && node.kind === SyntaxKind.ModuleDeclaration);
465            if (!moduleDeclaration) throw new Error('ModuleDeclaration not found');
466            const moduleBlock = (moduleDeclaration as any).body;
467            if (!moduleBlock) throw new Error('ModuleBlock not found');
468            const importEqualsDeclaration = moduleBlock.statements.find(node => node && node.kind === SyntaxKind.ImportEqualsDeclaration);
469            if (!importEqualsDeclaration) throw new Error('ImportEqualsDeclaration not found');
470            const node: Identifier = (importEqualsDeclaration as any)?.name;
471            const scope = scopeManager.getScopeOfNode(node);
472            assert.strictEqual(scope?.name, 'ns111');
473          });
474
475          it('get no scope of node', function () {
476            const anotherSourceFile = createSourceFile('another.ts', 'let y = 2;', ScriptTarget.ES2015, true);
477            const node = (anotherSourceFile.statements[0] as VariableStatement).declarationList.declarations[0].name;
478            const scope = scopeManager.getScopeOfNode(node);
479            assert.strictEqual(scope, undefined);
480          });
481        });
482      });
483    });
484  });
485});
486
487
488