/
githubmirror
/
angular
Обзор
Документация
Войти
/
githubmirror
/
angular
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
packages/compiler-cli/src/ngtsc/annotations/directive/test/directive_spec.ts
263 строки
8 KB
Matthieu Riegler
feat(compiler): Angular expressions with optional chaining returns `undefined`
29 апр 2026, 01:26
Не верифицирован
29 апр 2026, 01:26
2896c93
Код
Авторство
О чём код?
/** * @license * Copyright Google LLC 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.dev/license */ import { CssSelector, MatchSource, parseTemplate, R3TargetBinder, SelectorMatcher, DirectiveMeta as T2DirectiveMeta, TmplAstElement, } from '@angular/compiler'; import ts from 'typescript'; import {absoluteFrom} from '../../../file_system'; import {runInEachFileSystem} from '../../../file_system/testing'; import {ImportedSymbolsTracker, ReferenceEmitter} from '../../../imports'; import { CompoundMetadataReader, DtsMetadataReader, HostDirectivesResolver, LocalMetadataRegistry, ResourceRegistry, } from '../../../metadata'; import {PartialEvaluator} from '../../../partial_evaluator'; import {NOOP_PERF_RECORDER} from '../../../perf'; import { ClassDeclaration, isNamedClassDeclaration, TypeScriptReflectionHost, } from '../../../reflection'; import { LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver, TypeCheckScopeRegistry, } from '../../../scope'; import {getDeclaration, makeProgram} from '../../../testing'; import {CompilationMode} from '../../../transform'; import { InjectableClassRegistry, JitDeclarationRegistry, NoopReferencesRegistry, } from '../../common'; import {DirectiveDecoratorHandler} from '../index'; runInEachFileSystem(() => { let _: typeof absoluteFrom; beforeEach(() => (_ = absoluteFrom)); 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 analysis1 = analyzeDirective(program, 'TestDir1', /*hasBaseClass*/ false); expect(analysis1.meta.usesInheritance).toBe(false); const analysis2 = analyzeDirective(program, 'TestDir2', /*hasBaseClass*/ true); expect(analysis2.meta.usesInheritance).toBe(true); }); it('should record the source span of a Directive class type', () => { const src = ` import {Directive} from '@angular/core'; @Directive({selector: 'test-dir'}) export class TestDir {} `; const {program} = makeProgram([ { name: _('/node_modules/@angular/core/index.d.ts'), contents: 'export const Directive: any;', }, { name: _('/entry.ts'), contents: src, }, ]); const analysis = analyzeDirective(program, 'TestDir'); const span = analysis.meta.typeSourceSpan; expect(span.toString()).toBe('TestDir'); expect(span.start.toString()).toContain('/entry.ts@5:22'); expect(span.end.toString()).toContain('/entry.ts@5:29'); }); it('should produce metadata compatible with template binding', () => { const src = ` import {Directive, Input} from '@angular/core'; @Directive({selector: '[dir]'}) export class TestDir { @Input('propName') fieldName: string; } `; const {program} = makeProgram([ { name: _('/node_modules/@angular/core/index.d.ts'), contents: 'export const Directive: any; export const Input: any;', }, { name: _('/entry.ts'), contents: src, }, ]); const analysis = analyzeDirective(program, 'TestDir'); const matcher = new SelectorMatcher<T2DirectiveMeta[]>(); const dirMeta: T2DirectiveMeta = { ref: { key: 'TestDir', }, exportAs: null, inputs: analysis.inputs, outputs: analysis.outputs, isComponent: false, name: 'Dir', selector: '[dir]', isStructural: false, animationTriggerNames: null, ngContentSelectors: null, preserveWhitespaces: false, matchSource: MatchSource.Selector, }; matcher.addSelectables(CssSelector.parse('[dir]'), [dirMeta]); const {nodes} = parseTemplate('<div dir [propName]="expr"></div>', 'unimportant.html'); const binder = new R3TargetBinder(matcher).bind({template: nodes}); const propBinding = (nodes[0] as TmplAstElement).inputs[0]; const propBindingConsumer = binder.getConsumerOfBinding(propBinding); // Assert that the consumer of the binding is the directive, which means that the metadata // fed into the SelectorMatcher was compatible with the binder, and did not confuse property // and field names. expect(propBindingConsumer).toBe(dirMeta); }); it('should identify a structural directive', () => { const src = ` import {Directive, TemplateRef} from '@angular/core'; @Directive({selector: 'test-dir'}) export class TestDir { constructor(private ref: TemplateRef) {} } `; const {program} = makeProgram([ { name: _('/node_modules/@angular/core/index.d.ts'), contents: 'export const Directive: any; export declare class TemplateRef {}', }, { name: _('/entry.ts'), contents: src, }, ]); const analysis = analyzeDirective(program, 'TestDir'); expect(analysis.isStructural).toBeTrue(); }); }); // Helpers function analyzeDirective(program: ts.Program, dirName: string, hasBaseClass: boolean = false) { class TestReflectionHost extends TypeScriptReflectionHost { constructor(checker: ts.TypeChecker) { super(checker); } override hasBaseClass(_class: ClassDeclaration): boolean { return hasBaseClass; } } const checker = program.getTypeChecker(); const reflectionHost = new TestReflectionHost(checker); const evaluator = new PartialEvaluator(reflectionHost, checker, /*dependencyTracker*/ null); const metaReader = new LocalMetadataRegistry(); const dtsReader = new DtsMetadataReader(checker, reflectionHost); const refEmitter = new ReferenceEmitter([]); const referenceRegistry = new NoopReferencesRegistry(); const scopeRegistry = new LocalModuleScopeRegistry( metaReader, new CompoundMetadataReader([metaReader, dtsReader]), new MetadataDtsModuleScopeResolver(dtsReader, null), refEmitter, null, ); const injectableRegistry = new InjectableClassRegistry(reflectionHost, /* isCore */ false); const importTracker = new ImportedSymbolsTracker(); const jitDeclarationRegistry = new JitDeclarationRegistry(); const resourceRegistry = new ResourceRegistry(); const hostDirectivesResolver = new HostDirectivesResolver(metaReader); const typeCheckScopeRegistry = new TypeCheckScopeRegistry( scopeRegistry, metaReader, hostDirectivesResolver, ); const handler = new DirectiveDecoratorHandler( reflectionHost, evaluator, scopeRegistry, scopeRegistry, metaReader, injectableRegistry, refEmitter, referenceRegistry, /*isCore*/ false, /*strictCtorDeps*/ false, /*semanticDepGraphUpdater*/ null, /*annotateForClosureCompiler*/ false, NOOP_PERF_RECORDER, importTracker, /*includeClassMetadata*/ true, typeCheckScopeRegistry, /*compilationMode */ CompilationMode.FULL, jitDeclarationRegistry, resourceRegistry, /* strictStandalone */ false, /* implicitStandaloneValue */ true, /* usePoisonedData */ false, /* typeCheckHostBindings */ true, /* emitDeclarationOnly */ false, /* legacyOptionalChaining */ false, ); 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; } });