Previously, several `ngtsc` and `ngcc` APIs dealing with class
declaration nodes used inconsistent types. For example, some methods of
the `DecoratorHandler` interface expected a `ts.Declaration` argument,
but actual `DecoratorHandler` implementations specified a stricter
`ts.ClassDeclaration` type.
As a result, the stricter methods would operate under the incorrect
assumption that their arguments were of type `ts.ClassDeclaration`,
while the actual arguments might be of different types (e.g. `ngcc`
would call them with `ts.FunctionDeclaration` or
`ts.VariableDeclaration` arguments, when compiling ES5 code).
Additionally, since we need those class declarations to be referenced in
other parts of the program, `ngtsc`/`ngcc` had to either repeatedly
check for `ts.isIdentifier(node.name)` or assume there was a `name`
identifier and use `node.name!`. While this assumption happens to be
true in the current implementation, working around type-checking is
error-prone (e.g. the assumption might stop being true in the future).
This commit fixes this by introducing a new type to be used for such
class declarations (`ts.Declaration & {name: ts.Identifier}`) and using
it consistently throughput the code.
PR Close #29209
		
	
			
		
			
				
	
	
		
			84 lines
		
	
	
		
			2.9 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			84 lines
		
	
	
		
			2.9 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 ts from 'typescript';
 | |
| 
 | |
| import {NOOP_DEFAULT_IMPORT_RECORDER, ReferenceEmitter} from '../../imports';
 | |
| import {PartialEvaluator} from '../../partial_evaluator';
 | |
| import {TypeScriptReflectionHost} from '../../reflection';
 | |
| import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../scope';
 | |
| import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript';
 | |
| import {isNamedClassDeclaration} from '../../util/src/typescript';
 | |
| import {DirectiveDecoratorHandler} from '../src/directive';
 | |
| 
 | |
| 
 | |
| describe('DirectiveDecoratorHandler', () => {
 | |
|   it('should use the `ReflectionHost` to detect class inheritance', () => {
 | |
|     const {program} = makeProgram([
 | |
|       {
 | |
|         name: 'node_modules/@angular/core/index.d.ts',
 | |
|         contents: 'export const Directive: any;',
 | |
|       },
 | |
|       {
 | |
|         name: 'entry.ts',
 | |
|         contents: `
 | |
|           import {Directive} from '@angular/core';
 | |
| 
 | |
|           @Directive({selector: 'test-dir-1'})
 | |
|           export class TestDir1 {}
 | |
| 
 | |
|           @Directive({selector: 'test-dir-2'})
 | |
|           export class TestDir2 {}
 | |
|         `,
 | |
|       },
 | |
|     ]);
 | |
| 
 | |
|     const checker = program.getTypeChecker();
 | |
|     const reflectionHost = new TestReflectionHost(checker);
 | |
|     const evaluator = new PartialEvaluator(reflectionHost, checker);
 | |
|     const scopeRegistry = new LocalModuleScopeRegistry(
 | |
|         new MetadataDtsModuleScopeResolver(checker, reflectionHost, null), new ReferenceEmitter([]),
 | |
|         null);
 | |
|     const handler = new DirectiveDecoratorHandler(
 | |
|         reflectionHost, evaluator, scopeRegistry, NOOP_DEFAULT_IMPORT_RECORDER, false);
 | |
| 
 | |
|     const analyzeDirective = (dirName: string) => {
 | |
|       const DirNode = getDeclaration(program, 'entry.ts', dirName, isNamedClassDeclaration);
 | |
| 
 | |
|       const detected = handler.detect(DirNode, reflectionHost.getDecoratorsOfDeclaration(DirNode));
 | |
|       if (detected === undefined) {
 | |
|         throw new Error(`Failed to recognize @Directive (${dirName}).`);
 | |
|       }
 | |
| 
 | |
|       const {analysis} = handler.analyze(DirNode, detected.metadata);
 | |
|       if (analysis === undefined) {
 | |
|         throw new Error(`Failed to analyze @Directive (${dirName}).`);
 | |
|       }
 | |
| 
 | |
|       return analysis;
 | |
|     };
 | |
| 
 | |
|     // By default, `TestReflectionHost#hasBaseClass()` returns `false`.
 | |
|     const analysis1 = analyzeDirective('TestDir1');
 | |
|     expect(analysis1.meta.usesInheritance).toBe(false);
 | |
| 
 | |
|     // Tweak `TestReflectionHost#hasBaseClass()` to return true.
 | |
|     reflectionHost.hasBaseClassReturnValue = true;
 | |
| 
 | |
|     const analysis2 = analyzeDirective('TestDir2');
 | |
|     expect(analysis2.meta.usesInheritance).toBe(true);
 | |
|   });
 | |
| });
 | |
| 
 | |
| // Helpers
 | |
| class TestReflectionHost extends TypeScriptReflectionHost {
 | |
|   hasBaseClassReturnValue = false;
 | |
| 
 | |
|   hasBaseClass(node: ts.Declaration): boolean { return this.hasBaseClassReturnValue; }
 | |
| }
 |