This commit refactors TS-only utility functions to a separate file so that they could be shared with Ivy language service. A separate ts_library rule is created so that there is no dependency on `compiler` and `compiler-cli` to make the compilation fast and light-weight. The method `getPropertyAssignmentFromValue` is modified slightly to improve the ergonomics of the function. PR Close #36984
		
			
				
	
	
		
			124 lines
		
	
	
		
			4.1 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			124 lines
		
	
	
		
			4.1 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| /**
 | |
|  * @license
 | |
|  * Copyright Google Inc. All Rights Reserved.
 | |
|  *
 | |
|  * Use of this source code is governed by an MIT-style license that can be
 | |
|  * found in the LICENSE file at https://angular.io/license
 | |
|  */
 | |
| 
 | |
| import * as ng from '@angular/compiler';
 | |
| import * as ts from 'typescript';
 | |
| 
 | |
| import {getClassDeclFromDecoratorProp, getDirectiveClassLike} from '../src/ts_utils';
 | |
| import {getPathToNodeAtPosition} from '../src/utils';
 | |
| import {MockTypescriptHost} from './test_utils';
 | |
| 
 | |
| describe('getDirectiveClassLike', () => {
 | |
|   it('should return a directive class', () => {
 | |
|     const sourceFile = ts.createSourceFile(
 | |
|         'foo.ts', `
 | |
|       @NgModule({
 | |
|         declarations: [],
 | |
|       })
 | |
|       class AppModule {}
 | |
|     `,
 | |
|         ts.ScriptTarget.ES2015);
 | |
|     const result = sourceFile.forEachChild(c => {
 | |
|       const directive = getDirectiveClassLike(c);
 | |
|       if (directive) {
 | |
|         return directive;
 | |
|       }
 | |
|     });
 | |
|     expect(result).toBeTruthy();
 | |
|     const {decoratorId, classId} = result!;
 | |
|     expect(decoratorId.kind).toBe(ts.SyntaxKind.Identifier);
 | |
|     expect(decoratorId.text).toBe('NgModule');
 | |
|     expect(classId.text).toBe('AppModule');
 | |
|   });
 | |
| });
 | |
| 
 | |
| describe('getPathToNodeAtPosition', () => {
 | |
|   const html = '<div c></div>';
 | |
|   const nodes: ng.Node[] = [];
 | |
| 
 | |
|   beforeAll(() => {
 | |
|     const parser = new ng.HtmlParser();
 | |
|     const {rootNodes, errors} = parser.parse(html, 'url');
 | |
|     expect(errors.length).toBe(0);
 | |
|     nodes.push(...rootNodes);
 | |
|   });
 | |
| 
 | |
|   it('should capture element', () => {
 | |
|     // Try to get a path to an element
 | |
|     // <|div c></div>
 | |
|     //  ^ cursor is here
 | |
|     const position = html.indexOf('div');
 | |
|     const path = getPathToNodeAtPosition(nodes, position);
 | |
|     // There should be just 1 node in the path, the Element node
 | |
|     expect(path.empty).toBe(false);
 | |
|     expect(path.head instanceof ng.Element).toBe(true);
 | |
|     expect(path.head).toBe(path.tail);
 | |
|   });
 | |
| 
 | |
|   it('should capture attribute', () => {
 | |
|     // Try to get a path to an attribute
 | |
|     // <div |c></div>
 | |
|     //      ^ cusor is here, before the attribute
 | |
|     const position = html.indexOf('c');
 | |
|     const path = getPathToNodeAtPosition(nodes, position);
 | |
|     expect(path.empty).toBe(false);
 | |
|     expect(path.head instanceof ng.Element).toBe(true);
 | |
|     expect(path.tail instanceof ng.Attribute).toBe(true);
 | |
|   });
 | |
| 
 | |
|   it('should capture attribute before cursor', () => {
 | |
|     // Try to get a path to an attribute
 | |
|     // <div c|></div>
 | |
|     //       ^ cursor is here, after the attribute
 | |
|     const position = html.indexOf('c') + 1;
 | |
|     const path = getPathToNodeAtPosition(nodes, position);
 | |
|     expect(path.empty).toBe(false);
 | |
|     expect(path.head instanceof ng.Element).toBe(true);
 | |
|     expect(path.tail instanceof ng.Attribute).toBe(true);
 | |
|   });
 | |
| });
 | |
| 
 | |
| describe('getClassDeclFromTemplateNode', () => {
 | |
|   it('should find class declaration in syntax-only mode', () => {
 | |
|     const sourceFile = ts.createSourceFile(
 | |
|         'foo.ts', `
 | |
|         @Component({
 | |
|           template: '<div></div>'
 | |
|         })
 | |
|         class MyComponent {}`,
 | |
|         ts.ScriptTarget.ES2015, true /* setParentNodes */);
 | |
|     function visit(node: ts.Node): ts.ClassDeclaration|undefined {
 | |
|       if (ts.isPropertyAssignment(node)) {
 | |
|         return getClassDeclFromDecoratorProp(node);
 | |
|       }
 | |
|       return node.forEachChild(visit);
 | |
|     }
 | |
|     const classDecl = sourceFile.forEachChild(visit);
 | |
|     expect(classDecl).toBeTruthy();
 | |
|     expect(classDecl!.kind).toBe(ts.SyntaxKind.ClassDeclaration);
 | |
|     expect((classDecl as ts.ClassDeclaration).name!.text).toBe('MyComponent');
 | |
|   });
 | |
| 
 | |
| 
 | |
|   it('should return class declaration for AppComponent', () => {
 | |
|     const host = new MockTypescriptHost(['/app/app.component.ts']);
 | |
|     const tsLS = ts.createLanguageService(host);
 | |
|     const sourceFile = tsLS.getProgram()!.getSourceFile('/app/app.component.ts');
 | |
|     expect(sourceFile).toBeTruthy();
 | |
|     const classDecl = sourceFile!.forEachChild(function visit(node): ts.Node|undefined {
 | |
|       if (ts.isPropertyAssignment(node)) {
 | |
|         return getClassDeclFromDecoratorProp(node);
 | |
|       }
 | |
|       return node.forEachChild(visit);
 | |
|     });
 | |
|     expect(classDecl).toBeTruthy();
 | |
|     expect(ts.isClassDeclaration(classDecl!)).toBe(true);
 | |
|     expect((classDecl as ts.ClassDeclaration).name!.text).toBe('AppComponent');
 | |
|   });
 | |
| });
 |