From 4550a81bdc130b2ea6bd365e9d5547afbf8850b2 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Wed, 8 Nov 2023 10:57:58 +0100 Subject: [PATCH] refactor(compiler-cli): expose ng-content selectors and preserveWhitespaces during template type checking (#52726) These changes expose the `ngContentSelectors` and `preserveWhitespaces` metadata to the TCB so they can be used in the next commit to implement a new diagnostic. PR Close #52726 --- .../ngtsc/annotations/component/src/handler.ts | 9 ++++----- .../ngtsc/annotations/directive/src/handler.ts | 2 ++ .../annotations/directive/test/directive_spec.ts | 2 ++ .../compiler-cli/src/ngtsc/indexer/test/util.ts | 2 ++ .../compiler-cli/src/ngtsc/metadata/src/dts.ts | 7 +++++++ .../src/ngtsc/scope/test/local_spec.ts | 2 ++ .../compiler-cli/src/ngtsc/typecheck/api/api.ts | 5 +++++ .../src/ngtsc/typecheck/api/context.ts | 4 +++- .../src/ngtsc/typecheck/src/context.ts | 5 +++-- .../src/ngtsc/typecheck/src/type_check_block.ts | 5 +++-- .../src/ngtsc/typecheck/testing/index.ts | 11 +++++++++-- packages/compiler/src/render3/view/api.ts | 5 +++++ packages/compiler/src/render3/view/t2_api.ts | 13 +++++++++++++ .../compiler/test/render3/view/binding_spec.ts | 16 ++++++++++++++++ 14 files changed, 76 insertions(+), 12 deletions(-) diff --git a/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts b/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts index 49592bccb109f..402e7ffe1c0d8 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts @@ -463,10 +463,7 @@ export class ComponentDecoratorHandler implements rawHostDirectives, meta: { ...metadata, - template: { - nodes: template.nodes, - ngContentSelectors: template.ngContentSelectors, - }, + template, encapsulation, changeDetection, interpolation: template.interpolationConfig ?? DEFAULT_INTERPOLATION_CONFIG, @@ -546,6 +543,8 @@ export class ComponentDecoratorHandler implements schemas: analysis.schemas, decorator: analysis.decorator, assumedToExportProviders: false, + ngContentSelectors: analysis.template.ngContentSelectors, + preserveWhitespaces: analysis.template.preserveWhitespaces ?? false, }); this.resourceRegistry.registerResources(analysis.resources, node); @@ -614,7 +613,7 @@ export class ComponentDecoratorHandler implements ctx.addTemplate( new Reference(node), binder, meta.template.diagNodes, scope.pipes, scope.schemas, meta.template.sourceMapping, meta.template.file, meta.template.errors, - meta.meta.isStandalone); + meta.meta.isStandalone, meta.meta.template.preserveWhitespaces ?? false); } extendedTemplateCheck( diff --git a/packages/compiler-cli/src/ngtsc/annotations/directive/src/handler.ts b/packages/compiler-cli/src/ngtsc/annotations/directive/src/handler.ts index 0c7310711fbd3..b17630770cbf2 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/directive/src/handler.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/directive/src/handler.ts @@ -169,7 +169,9 @@ export class DirectiveDecoratorHandler implements isSignal: analysis.meta.isSignal, imports: null, schemas: null, + ngContentSelectors: null, decorator: analysis.decorator, + preserveWhitespaces: false, // Directives analyzed within our own compilation are not _assumed_ to export providers. // Instead, we statically analyze their imports to make a direct determination. assumedToExportProviders: false, diff --git a/packages/compiler-cli/src/ngtsc/annotations/directive/test/directive_spec.ts b/packages/compiler-cli/src/ngtsc/annotations/directive/test/directive_spec.ts index 6b2f2308fdd0d..b0cede22afc2a 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/directive/test/directive_spec.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/directive/test/directive_spec.ts @@ -110,6 +110,8 @@ runInEachFileSystem(() => { selector: '[dir]', isStructural: false, animationTriggerNames: null, + ngContentSelectors: null, + preserveWhitespaces: false, }; matcher.addSelectables(CssSelector.parse('[dir]'), [dirMeta]); diff --git a/packages/compiler-cli/src/ngtsc/indexer/test/util.ts b/packages/compiler-cli/src/ngtsc/indexer/test/util.ts index e2d287758de36..2907c6291e711 100644 --- a/packages/compiler-cli/src/ngtsc/indexer/test/util.ts +++ b/packages/compiler-cli/src/ngtsc/indexer/test/util.ts @@ -56,6 +56,8 @@ export function getBoundTemplate( exportAs: null, isStructural: false, animationTriggerNames: null, + ngContentSelectors: null, + preserveWhitespaces: false, }]); }); const binder = new R3TargetBinder(matcher); diff --git a/packages/compiler-cli/src/ngtsc/metadata/src/dts.ts b/packages/compiler-cli/src/ngtsc/metadata/src/dts.ts index eb91b3bb2b848..3f3d4ce29ce64 100644 --- a/packages/compiler-cli/src/ngtsc/metadata/src/dts.ts +++ b/packages/compiler-cli/src/ngtsc/metadata/src/dts.ts @@ -96,6 +96,9 @@ export class DtsMetadataReader implements MetadataReader { param.typeValueReference.importedName === 'TemplateRef'; }); + const ngContentSelectors = + def.type.typeArguments.length > 6 ? readStringArrayType(def.type.typeArguments[6]) : null; + const isStandalone = def.type.typeArguments.length > 7 && (readBooleanType(def.type.typeArguments[7]) ?? false); @@ -126,6 +129,7 @@ export class DtsMetadataReader implements MetadataReader { isPoisoned: false, isStructural, animationTriggerNames: null, + ngContentSelectors, isStandalone, isSignal, // Imports are tracked in metadata only for template type-checking purposes, @@ -136,6 +140,9 @@ export class DtsMetadataReader implements MetadataReader { decorator: null, // Assume that standalone components from .d.ts files may export providers. assumedToExportProviders: isComponent && isStandalone, + // `preserveWhitespaces` isn't encoded in the .d.ts and is only + // used to increase the accuracy of a diagnostic. + preserveWhitespaces: false, }; } diff --git a/packages/compiler-cli/src/ngtsc/scope/test/local_spec.ts b/packages/compiler-cli/src/ngtsc/scope/test/local_spec.ts index 14f54199b34d0..64aa131addb12 100644 --- a/packages/compiler-cli/src/ngtsc/scope/test/local_spec.ts +++ b/packages/compiler-cli/src/ngtsc/scope/test/local_spec.ts @@ -330,6 +330,8 @@ function fakeDirective(ref: Reference): DirectiveMeta { decorator: null, hostDirectives: null, assumedToExportProviders: false, + ngContentSelectors: null, + preserveWhitespaces: false, }; } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/api/api.ts b/packages/compiler-cli/src/ngtsc/typecheck/api/api.ts index 5bdc53ef0409e..11e82931d560f 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/api/api.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/api/api.ts @@ -84,6 +84,11 @@ export interface TypeCheckBlockMetadata { * A boolean indicating whether the component is standalone. */ isStandalone: boolean; + + /** + * A boolean indicating whether the component preserves whitespaces in its template. + */ + preserveWhitespaces: boolean; } export interface TypeCtorMetadata { diff --git a/packages/compiler-cli/src/ngtsc/typecheck/api/context.ts b/packages/compiler-cli/src/ngtsc/typecheck/api/context.ts index 747477a0dc04e..7967fb007dc19 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/api/context.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/api/context.ts @@ -37,13 +37,15 @@ export interface TypeCheckContext { * @param file the `ParseSourceFile` associated with the template. * @param parseErrors the `ParseError`'s associated with the template. * @param isStandalone a boolean indicating whether the component is standalone. + * @param preserveWhitespaces a boolean indicating whether the component's template preserves + * whitespaces. */ addTemplate( ref: Reference>, binder: R3TargetBinder, template: TmplAstNode[], pipes: Map>>, schemas: SchemaMetadata[], sourceMapping: TemplateSourceMapping, file: ParseSourceFile, - parseErrors: ParseError[]|null, isStandalone: boolean): void; + parseErrors: ParseError[]|null, isStandalone: boolean, preserveWhitespaces: boolean): void; } /** diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts index 075e7d28c03e5..c9fdd2e2fb934 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts @@ -210,7 +210,7 @@ export class TypeCheckContextImpl implements TypeCheckContext { binder: R3TargetBinder, template: TmplAstNode[], pipes: Map>>, schemas: SchemaMetadata[], sourceMapping: TemplateSourceMapping, file: ParseSourceFile, - parseErrors: ParseError[]|null, isStandalone: boolean): void { + parseErrors: ParseError[]|null, isStandalone: boolean, preserveWhitespaces: boolean): void { if (!this.host.shouldCheckComponent(ref.node)) { return; } @@ -293,7 +293,8 @@ export class TypeCheckContextImpl implements TypeCheckContext { boundTarget, pipes, schemas, - isStandalone + isStandalone, + preserveWhitespaces, }; this.perf.eventCount(PerfEvent.GenerateTcb); if (inliningRequirement !== TcbInliningRequirement.None && diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts index dda37e07da0fa..d00b33b243cab 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts @@ -84,7 +84,7 @@ export function generateTypeCheckBlock( genericContextBehavior: TcbGenericContextBehavior): ts.FunctionDeclaration { const tcb = new Context( env, domSchemaChecker, oobRecorder, meta.id, meta.boundTarget, meta.pipes, meta.schemas, - meta.isStandalone); + meta.isStandalone, meta.preserveWhitespaces); const scope = Scope.forNodes(tcb, null, null, tcb.boundTarget.target.template!, /* guard */ null); const ctxRawType = env.referenceType(ref); if (!ts.isTypeReferenceNode(ctxRawType)) { @@ -1488,7 +1488,8 @@ export class Context { readonly oobRecorder: OutOfBandDiagnosticRecorder, readonly id: TemplateId, readonly boundTarget: BoundTarget, private pipes: Map>>, - readonly schemas: SchemaMetadata[], readonly hostIsStandalone: boolean) {} + readonly schemas: SchemaMetadata[], readonly hostIsStandalone: boolean, + readonly hostPreserveWhitespaces: boolean) {} /** * Allocate a new variable name for use within the `Context`. diff --git a/packages/compiler-cli/src/ngtsc/typecheck/testing/index.ts b/packages/compiler-cli/src/ngtsc/typecheck/testing/index.ts index 51bf3d900a359..10526facbf105 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/testing/index.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/testing/index.ts @@ -249,6 +249,8 @@ export interface TestDirective extends Partial { return { directive: new Reference(resolveDeclaration(hostDecl.directive)), @@ -732,6 +737,8 @@ function makeScope(program: ts.Program, sf: ts.SourceFile, decls: TestDeclaratio schemas: null, decorator: null, assumedToExportProviders: false, + ngContentSelectors: decl.ngContentSelectors || null, + preserveWhitespaces: decl.preserveWhitespaces ?? false, hostDirectives: decl.hostDirectives === undefined ? null : decl.hostDirectives.map(hostDecl => { return { diff --git a/packages/compiler/src/render3/view/api.ts b/packages/compiler/src/render3/view/api.ts index 74363df18e8d7..bee4ce5b0128a 100644 --- a/packages/compiler/src/render3/view/api.ts +++ b/packages/compiler/src/render3/view/api.ts @@ -226,6 +226,11 @@ export interface R3ComponentMetadata * element without selector is present. */ ngContentSelectors: string[]; + + /** + * Whether the template preserves whitespaces from the user's code. + */ + preserveWhitespaces?: boolean; }; declarations: DeclarationT[]; diff --git a/packages/compiler/src/render3/view/t2_api.ts b/packages/compiler/src/render3/view/t2_api.ts index f4e9662948d02..689d28cc381ca 100644 --- a/packages/compiler/src/render3/view/t2_api.ts +++ b/packages/compiler/src/render3/view/t2_api.ts @@ -93,8 +93,21 @@ export interface DirectiveMeta { */ exportAs: string[]|null; + /** + * Whether the directive is a structural directive (e.g. `
`). + */ isStructural: boolean; + /** + * If the directive is a component, includes the selectors of its `ng-content` elements. + */ + ngContentSelectors: string[]|null; + + /** + * Whether the template of the component preserves whitespaces. + */ + preserveWhitespaces: boolean; + /** * The name of animations that the user defines in the component. * Only includes the animation names. diff --git a/packages/compiler/test/render3/view/binding_spec.ts b/packages/compiler/test/render3/view/binding_spec.ts index eb682c3fd2b60..46c6a09258b4a 100644 --- a/packages/compiler/test/render3/view/binding_spec.ts +++ b/packages/compiler/test/render3/view/binding_spec.ts @@ -41,6 +41,8 @@ function makeSelectorMatcher(): SelectorMatcher { isStructural: true, selector: '[ngFor][ngForOf]', animationTriggerNames: null, + ngContentSelectors: null, + preserveWhitespaces: false, }]); matcher.addSelectables(CssSelector.parse('[dir]'), [{ name: 'Dir', @@ -51,6 +53,8 @@ function makeSelectorMatcher(): SelectorMatcher { isStructural: false, selector: '[dir]', animationTriggerNames: null, + ngContentSelectors: null, + preserveWhitespaces: false, }]); matcher.addSelectables(CssSelector.parse('[hasOutput]'), [{ name: 'HasOutput', @@ -61,6 +65,8 @@ function makeSelectorMatcher(): SelectorMatcher { isStructural: false, selector: '[hasOutput]', animationTriggerNames: null, + ngContentSelectors: null, + preserveWhitespaces: false, }]); matcher.addSelectables(CssSelector.parse('[hasInput]'), [{ name: 'HasInput', @@ -71,6 +77,8 @@ function makeSelectorMatcher(): SelectorMatcher { isStructural: false, selector: '[hasInput]', animationTriggerNames: null, + ngContentSelectors: null, + preserveWhitespaces: false, }]); matcher.addSelectables(CssSelector.parse('[sameSelectorAsInput]'), [{ name: 'SameSelectorAsInput', @@ -81,6 +89,8 @@ function makeSelectorMatcher(): SelectorMatcher { isStructural: false, selector: '[sameSelectorAsInput]', animationTriggerNames: null, + ngContentSelectors: null, + preserveWhitespaces: false, }]); matcher.addSelectables(CssSelector.parse('comp'), [{ name: 'Comp', @@ -91,6 +101,8 @@ function makeSelectorMatcher(): SelectorMatcher { isStructural: false, selector: 'comp', animationTriggerNames: null, + ngContentSelectors: null, + preserveWhitespaces: false, }]); const simpleDirectives = ['a', 'b', 'c', 'd', 'e', 'f']; @@ -106,6 +118,8 @@ function makeSelectorMatcher(): SelectorMatcher { isStructural: true, selector: `[${dir}]`, animationTriggerNames: null, + ngContentSelectors: null, + preserveWhitespaces: false, }]); } @@ -155,6 +169,8 @@ describe('t2 binding', () => { isStructural: false, selector: 'text[dir]', animationTriggerNames: null, + ngContentSelectors: null, + preserveWhitespaces: false, }]); const binder = new R3TargetBinder(matcher); const res = binder.bind({template: template.nodes});