Skip to content

Commit

Permalink
refactor(compiler-cli): expose ng-content selectors and preserveWhite…
Browse files Browse the repository at this point in the history
…spaces during template type checking (angular#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 angular#52726
  • Loading branch information
crisbeto authored and AndrewKushnir committed Nov 17, 2023
1 parent adbea7b commit 4550a81
Show file tree
Hide file tree
Showing 14 changed files with 76 additions and 12 deletions.
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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(
Expand Down
Expand Up @@ -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,
Expand Down
Expand Up @@ -110,6 +110,8 @@ runInEachFileSystem(() => {
selector: '[dir]',
isStructural: false,
animationTriggerNames: null,
ngContentSelectors: null,
preserveWhitespaces: false,
};
matcher.addSelectables(CssSelector.parse('[dir]'), [dirMeta]);

Expand Down
2 changes: 2 additions & 0 deletions packages/compiler-cli/src/ngtsc/indexer/test/util.ts
Expand Up @@ -56,6 +56,8 @@ export function getBoundTemplate(
exportAs: null,
isStructural: false,
animationTriggerNames: null,
ngContentSelectors: null,
preserveWhitespaces: false,
}]);
});
const binder = new R3TargetBinder(matcher);
Expand Down
7 changes: 7 additions & 0 deletions packages/compiler-cli/src/ngtsc/metadata/src/dts.ts
Expand Up @@ -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);

Expand Down Expand Up @@ -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,
Expand All @@ -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,
};
}

Expand Down
2 changes: 2 additions & 0 deletions packages/compiler-cli/src/ngtsc/scope/test/local_spec.ts
Expand Up @@ -330,6 +330,8 @@ function fakeDirective(ref: Reference<ClassDeclaration>): DirectiveMeta {
decorator: null,
hostDirectives: null,
assumedToExportProviders: false,
ngContentSelectors: null,
preserveWhitespaces: false,
};
}

Expand Down
5 changes: 5 additions & 0 deletions packages/compiler-cli/src/ngtsc/typecheck/api/api.ts
Expand Up @@ -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 {
Expand Down
4 changes: 3 additions & 1 deletion packages/compiler-cli/src/ngtsc/typecheck/api/context.ts
Expand Up @@ -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<ClassDeclaration<ts.ClassDeclaration>>,
binder: R3TargetBinder<TypeCheckableDirectiveMeta>, template: TmplAstNode[],
pipes: Map<string, Reference<ClassDeclaration<ts.ClassDeclaration>>>,
schemas: SchemaMetadata[], sourceMapping: TemplateSourceMapping, file: ParseSourceFile,
parseErrors: ParseError[]|null, isStandalone: boolean): void;
parseErrors: ParseError[]|null, isStandalone: boolean, preserveWhitespaces: boolean): void;
}

/**
Expand Down
5 changes: 3 additions & 2 deletions packages/compiler-cli/src/ngtsc/typecheck/src/context.ts
Expand Up @@ -210,7 +210,7 @@ export class TypeCheckContextImpl implements TypeCheckContext {
binder: R3TargetBinder<TypeCheckableDirectiveMeta>, template: TmplAstNode[],
pipes: Map<string, Reference<ClassDeclaration<ts.ClassDeclaration>>>,
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;
}
Expand Down Expand Up @@ -293,7 +293,8 @@ export class TypeCheckContextImpl implements TypeCheckContext {
boundTarget,
pipes,
schemas,
isStandalone
isStandalone,
preserveWhitespaces,
};
this.perf.eventCount(PerfEvent.GenerateTcb);
if (inliningRequirement !== TcbInliningRequirement.None &&
Expand Down
Expand Up @@ -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)) {
Expand Down Expand Up @@ -1488,7 +1488,8 @@ export class Context {
readonly oobRecorder: OutOfBandDiagnosticRecorder, readonly id: TemplateId,
readonly boundTarget: BoundTarget<TypeCheckableDirectiveMeta>,
private pipes: Map<string, Reference<ClassDeclaration<ts.ClassDeclaration>>>,
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`.
Expand Down
11 changes: 9 additions & 2 deletions packages/compiler-cli/src/ngtsc/typecheck/testing/index.ts
Expand Up @@ -249,6 +249,8 @@ export interface TestDirective extends Partial<Pick<
undeclaredInputFields?: string[];
isGeneric?: boolean;
code?: string;
ngContentSelectors?: string[]|null;
preserveWhitespaces?: boolean;
hostDirectives?: {
directive: TestDirective&{isStandalone: true},
inputs?: string[],
Expand Down Expand Up @@ -302,7 +304,8 @@ export function tcb(
const boundTarget = binder.bind({template: nodes});

const id = 'tcb' as TemplateId;
const meta: TypeCheckBlockMetadata = {id, boundTarget, pipes, schemas: [], isStandalone: false};
const meta: TypeCheckBlockMetadata =
{id, boundTarget, pipes, schemas: [], isStandalone: false, preserveWhitespaces: false};

const fullConfig: TypeCheckingConfig = {
applyTemplateContextGuards: true,
Expand Down Expand Up @@ -506,7 +509,7 @@ export function setup(targets: TypeCheckingTarget[], overrides: {
};

ctx.addTemplate(
classRef, binder, nodes, pipes, [], sourceMapping, templateFile, errors, false);
classRef, binder, nodes, pipes, [], sourceMapping, templateFile, errors, false, false);
}
}
});
Expand Down Expand Up @@ -677,6 +680,8 @@ function getDirectiveMetaFromDeclaration(
baseClass: null,
animationTriggerNames: null,
decorator: null,
ngContentSelectors: decl.ngContentSelectors || null,
preserveWhitespaces: decl.preserveWhitespaces ?? false,
hostDirectives: decl.hostDirectives === undefined ? null : decl.hostDirectives.map(hostDecl => {
return {
directive: new Reference(resolveDeclaration(hostDecl.directive)),
Expand Down Expand Up @@ -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 {
Expand Down
5 changes: 5 additions & 0 deletions packages/compiler/src/render3/view/api.ts
Expand Up @@ -226,6 +226,11 @@ export interface R3ComponentMetadata<DeclarationT extends R3TemplateDependency>
* element without selector is present.
*/
ngContentSelectors: string[];

/**
* Whether the template preserves whitespaces from the user's code.
*/
preserveWhitespaces?: boolean;
};

declarations: DeclarationT[];
Expand Down
13 changes: 13 additions & 0 deletions packages/compiler/src/render3/view/t2_api.ts
Expand Up @@ -93,8 +93,21 @@ export interface DirectiveMeta {
*/
exportAs: string[]|null;

/**
* Whether the directive is a structural directive (e.g. `<div *ngIf></div>`).
*/
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.
Expand Down
16 changes: 16 additions & 0 deletions packages/compiler/test/render3/view/binding_spec.ts
Expand Up @@ -41,6 +41,8 @@ function makeSelectorMatcher(): SelectorMatcher<DirectiveMeta[]> {
isStructural: true,
selector: '[ngFor][ngForOf]',
animationTriggerNames: null,
ngContentSelectors: null,
preserveWhitespaces: false,
}]);
matcher.addSelectables(CssSelector.parse('[dir]'), [{
name: 'Dir',
Expand All @@ -51,6 +53,8 @@ function makeSelectorMatcher(): SelectorMatcher<DirectiveMeta[]> {
isStructural: false,
selector: '[dir]',
animationTriggerNames: null,
ngContentSelectors: null,
preserveWhitespaces: false,
}]);
matcher.addSelectables(CssSelector.parse('[hasOutput]'), [{
name: 'HasOutput',
Expand All @@ -61,6 +65,8 @@ function makeSelectorMatcher(): SelectorMatcher<DirectiveMeta[]> {
isStructural: false,
selector: '[hasOutput]',
animationTriggerNames: null,
ngContentSelectors: null,
preserveWhitespaces: false,
}]);
matcher.addSelectables(CssSelector.parse('[hasInput]'), [{
name: 'HasInput',
Expand All @@ -71,6 +77,8 @@ function makeSelectorMatcher(): SelectorMatcher<DirectiveMeta[]> {
isStructural: false,
selector: '[hasInput]',
animationTriggerNames: null,
ngContentSelectors: null,
preserveWhitespaces: false,
}]);
matcher.addSelectables(CssSelector.parse('[sameSelectorAsInput]'), [{
name: 'SameSelectorAsInput',
Expand All @@ -81,6 +89,8 @@ function makeSelectorMatcher(): SelectorMatcher<DirectiveMeta[]> {
isStructural: false,
selector: '[sameSelectorAsInput]',
animationTriggerNames: null,
ngContentSelectors: null,
preserveWhitespaces: false,
}]);
matcher.addSelectables(CssSelector.parse('comp'), [{
name: 'Comp',
Expand All @@ -91,6 +101,8 @@ function makeSelectorMatcher(): SelectorMatcher<DirectiveMeta[]> {
isStructural: false,
selector: 'comp',
animationTriggerNames: null,
ngContentSelectors: null,
preserveWhitespaces: false,
}]);

const simpleDirectives = ['a', 'b', 'c', 'd', 'e', 'f'];
Expand All @@ -106,6 +118,8 @@ function makeSelectorMatcher(): SelectorMatcher<DirectiveMeta[]> {
isStructural: true,
selector: `[${dir}]`,
animationTriggerNames: null,
ngContentSelectors: null,
preserveWhitespaces: false,
}]);
}

Expand Down Expand Up @@ -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});
Expand Down

0 comments on commit 4550a81

Please sign in to comment.