mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
refactor(compiler): add more information to template binder (#60977)
Updates the template binder to include information about directives owned by a specific component/directive node and the names of template symbols that don't exist. These will be used when generating the type check block. PR Close #60977
This commit is contained in:
parent
26cba0804f
commit
4b0f733cb4
5 changed files with 136 additions and 27 deletions
|
|
@ -826,7 +826,7 @@ function prepareDeclarations(
|
|||
for (const meta of directives) {
|
||||
registry.set(meta.name, [meta, ...hostDirectiveResolder.resolve(meta)]);
|
||||
}
|
||||
return {matcher: new SelectorlessMatcher<DirectiveMeta[]>(registry), pipes};
|
||||
return {matcher: new SelectorlessMatcher<DirectiveMeta>(registry), pipes};
|
||||
} else {
|
||||
const matcher = new SelectorMatcher<DirectiveMeta[]>();
|
||||
for (const meta of directives) {
|
||||
|
|
|
|||
|
|
@ -474,9 +474,9 @@ export class SelectorContext<T = any> {
|
|||
}
|
||||
|
||||
export class SelectorlessMatcher<T = unknown> {
|
||||
constructor(private registry: Map<string, T>) {}
|
||||
constructor(private registry: Map<string, T[]>) {}
|
||||
|
||||
match(name: string): T | null {
|
||||
return this.registry.get(name) ?? null;
|
||||
match(name: string): T[] {
|
||||
return this.registry.has(name) ? this.registry.get(name)! : [];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -272,5 +272,18 @@ export interface BoundTarget<DirectiveT extends DirectiveMeta> {
|
|||
/**
|
||||
* Whether a given node is located in a `@defer` block.
|
||||
*/
|
||||
isDeferred(node: Element | Component): boolean;
|
||||
isDeferred(node: Element): boolean;
|
||||
|
||||
/**
|
||||
* Returns the directives that are owned by a specific component/directive node. This is either
|
||||
* the directive being referenced itself or its host directives.
|
||||
* @param node Node for which to retrieve the owned directives.
|
||||
*/
|
||||
getOwnedDirectives(node: Component | Directive): DirectiveT[] | null;
|
||||
|
||||
/**
|
||||
* Checks whether a component/directive that was referenced directly in the template exists.
|
||||
* @param name Name of the component/directive.
|
||||
*/
|
||||
referencedDirectiveExists(name: string): boolean;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -93,7 +93,10 @@ type ReferenceMap<DirectiveT> = Map<
|
|||
>;
|
||||
|
||||
/** Mapping between AST nodes and the directives that have been matched on them. */
|
||||
type MatchedDirectives<DirectiveT> = Map<Template | Element | Component, DirectiveT[]>;
|
||||
type MatchedDirectives<DirectiveT> = Map<Template | Element | Component | Directive, DirectiveT[]>;
|
||||
|
||||
/** Mapping between AST nodes and the directives that they own. */
|
||||
type OwnedDirectives<DirectiveT> = Map<Component | Directive, DirectiveT[]>;
|
||||
|
||||
/**
|
||||
* Mapping between a scoped not and the template entities that exist in it.
|
||||
|
|
@ -162,7 +165,7 @@ export function findMatchingDirectivesAndPipes(template: string, directiveSelect
|
|||
/** Object used to match template nodes to directives. */
|
||||
export type DirectiveMatcher<DirectiveT extends DirectiveMeta> =
|
||||
| SelectorMatcher<DirectiveT[]>
|
||||
| SelectorlessMatcher<DirectiveT[]>;
|
||||
| SelectorlessMatcher<DirectiveT>;
|
||||
|
||||
/**
|
||||
* Processes `Target`s with a given set of directives and performs a binding operation, which
|
||||
|
|
@ -182,7 +185,9 @@ export class R3TargetBinder<DirectiveT extends DirectiveMeta> implements TargetB
|
|||
}
|
||||
|
||||
const directives: MatchedDirectives<DirectiveT> = new Map();
|
||||
const ownedDirectives: OwnedDirectives<DirectiveT> = new Map();
|
||||
const eagerDirectives: DirectiveT[] = [];
|
||||
const missingDirectives = new Set<string>();
|
||||
const bindings: BindingsMap<DirectiveT> = new Map();
|
||||
const references: ReferenceMap<DirectiveT> = new Map();
|
||||
const scopedNodeEntities: ScopedNodeEntities = new Map();
|
||||
|
|
@ -210,7 +215,9 @@ export class R3TargetBinder<DirectiveT extends DirectiveMeta> implements TargetB
|
|||
target.template,
|
||||
this.directiveMatcher,
|
||||
directives,
|
||||
ownedDirectives,
|
||||
eagerDirectives,
|
||||
missingDirectives,
|
||||
bindings,
|
||||
references,
|
||||
);
|
||||
|
|
@ -247,7 +254,9 @@ export class R3TargetBinder<DirectiveT extends DirectiveMeta> implements TargetB
|
|||
return new R3BoundTarget(
|
||||
target,
|
||||
directives,
|
||||
ownedDirectives,
|
||||
eagerDirectives,
|
||||
missingDirectives,
|
||||
bindings,
|
||||
references,
|
||||
expressions,
|
||||
|
|
@ -503,7 +512,9 @@ class DirectiveBinder<DirectiveT extends DirectiveMeta> implements Visitor {
|
|||
private constructor(
|
||||
private directiveMatcher: DirectiveMatcher<DirectiveT>,
|
||||
private directives: MatchedDirectives<DirectiveT>,
|
||||
private ownedDirectives: OwnedDirectives<DirectiveT>,
|
||||
private eagerDirectives: DirectiveT[],
|
||||
private missingDirectives: Set<string>,
|
||||
private bindings: BindingsMap<DirectiveT>,
|
||||
private references: ReferenceMap<DirectiveT>,
|
||||
) {}
|
||||
|
|
@ -524,14 +535,18 @@ class DirectiveBinder<DirectiveT extends DirectiveMeta> implements Visitor {
|
|||
template: Node[],
|
||||
directiveMatcher: DirectiveMatcher<DirectiveT>,
|
||||
directives: MatchedDirectives<DirectiveT>,
|
||||
ownedDirectives: OwnedDirectives<DirectiveT>,
|
||||
eagerDirectives: DirectiveT[],
|
||||
missingDirectives: Set<string>,
|
||||
bindings: BindingsMap<DirectiveT>,
|
||||
references: ReferenceMap<DirectiveT>,
|
||||
): void {
|
||||
const matcher = new DirectiveBinder(
|
||||
directiveMatcher,
|
||||
directives,
|
||||
ownedDirectives,
|
||||
eagerDirectives,
|
||||
missingDirectives,
|
||||
bindings,
|
||||
references,
|
||||
);
|
||||
|
|
@ -606,29 +621,30 @@ class DirectiveBinder<DirectiveT extends DirectiveMeta> implements Visitor {
|
|||
}
|
||||
|
||||
visitComponent(node: Component): void {
|
||||
const directives: DirectiveT[] = [];
|
||||
let componentMetas: DirectiveT[] | null = null;
|
||||
|
||||
if (this.directiveMatcher instanceof SelectorlessMatcher) {
|
||||
componentMetas = this.directiveMatcher.match(node.componentName);
|
||||
const componentMatches = this.directiveMatcher.match(node.componentName);
|
||||
const directives: DirectiveT[] = [];
|
||||
|
||||
if (componentMetas !== null) {
|
||||
directives.push(...componentMetas);
|
||||
if (componentMatches.length > 0) {
|
||||
directives.push(...componentMatches);
|
||||
this.ownedDirectives.set(node, componentMatches);
|
||||
} else {
|
||||
this.missingDirectives.add(node.componentName);
|
||||
}
|
||||
|
||||
for (const directive of node.directives) {
|
||||
const directiveMetas = this.directiveMatcher.match(directive.name);
|
||||
|
||||
if (directiveMetas !== null) {
|
||||
if (directiveMetas.length > 0) {
|
||||
directives.push(...directiveMetas);
|
||||
this.ownedDirectives.set(directive, directiveMetas);
|
||||
} else {
|
||||
this.missingDirectives.add(directive.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.trackMatchedDirectives(node, directives);
|
||||
|
||||
if (componentMetas !== null) {
|
||||
this.trackSelectorlessBindings(node, componentMetas);
|
||||
this.trackMatchedDirectives(node, directives);
|
||||
this.trackSelectorlessBindings(node, componentMatches);
|
||||
}
|
||||
|
||||
node.directives.forEach((directive) => directive.visit(this));
|
||||
|
|
@ -641,7 +657,7 @@ class DirectiveBinder<DirectiveT extends DirectiveMeta> implements Visitor {
|
|||
? this.directiveMatcher.match(node.name)
|
||||
: null;
|
||||
|
||||
if (directives !== null) {
|
||||
if (directives !== null && directives.length > 0) {
|
||||
this.trackSelectorlessBindings(node, directives);
|
||||
}
|
||||
}
|
||||
|
|
@ -663,8 +679,11 @@ class DirectiveBinder<DirectiveT extends DirectiveMeta> implements Visitor {
|
|||
for (const directive of node.directives) {
|
||||
const matchedDirectives = this.directiveMatcher.match(directive.name);
|
||||
|
||||
if (matchedDirectives !== null) {
|
||||
if (matchedDirectives.length > 0) {
|
||||
directives.push(...matchedDirectives);
|
||||
this.ownedDirectives.set(directive, matchedDirectives);
|
||||
} else {
|
||||
this.missingDirectives.add(directive.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -687,6 +706,10 @@ class DirectiveBinder<DirectiveT extends DirectiveMeta> implements Visitor {
|
|||
}
|
||||
|
||||
private trackSelectorlessBindings(node: Component | Directive, metas: DirectiveT[]): void {
|
||||
if (metas.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const setBinding = (
|
||||
meta: DirectiveT,
|
||||
attribute: BoundAttribute | BoundEvent | TextAttribute,
|
||||
|
|
@ -706,9 +729,7 @@ class DirectiveBinder<DirectiveT extends DirectiveMeta> implements Visitor {
|
|||
// TODO(crisbeto): currently it's unclear how references should behave under selectorless,
|
||||
// given that there's one named class which can bring in multiple host directives.
|
||||
// For the time being only register the first directive as the reference target.
|
||||
if (metas.length > 0) {
|
||||
node.references.forEach((ref) => this.references.set(ref, {directive: metas[0], node: node}));
|
||||
}
|
||||
node.references.forEach((ref) => this.references.set(ref, {directive: metas[0], node: node}));
|
||||
}
|
||||
|
||||
private trackSelectorMatchedBindings(node: Element | Template, directives: DirectiveT[]): void {
|
||||
|
|
@ -1118,7 +1139,9 @@ class R3BoundTarget<DirectiveT extends DirectiveMeta> implements BoundTarget<Dir
|
|||
constructor(
|
||||
readonly target: Target,
|
||||
private directives: MatchedDirectives<DirectiveT>,
|
||||
private ownedDirectives: OwnedDirectives<DirectiveT>,
|
||||
private eagerDirectives: DirectiveT[],
|
||||
private missingDirectives: Set<string>,
|
||||
private bindings: BindingsMap<DirectiveT>,
|
||||
private references: ReferenceMap<DirectiveT>,
|
||||
private exprTargets: Map<AST, TemplateEntity>,
|
||||
|
|
@ -1137,7 +1160,7 @@ class R3BoundTarget<DirectiveT extends DirectiveMeta> implements BoundTarget<Dir
|
|||
return this.scopedNodeEntities.get(node) ?? new Set();
|
||||
}
|
||||
|
||||
getDirectivesOfNode(node: Element | Template): DirectiveT[] | null {
|
||||
getDirectivesOfNode(node: Element | Template | Component): DirectiveT[] | null {
|
||||
return this.directives.get(node) || null;
|
||||
}
|
||||
|
||||
|
|
@ -1251,7 +1274,7 @@ class R3BoundTarget<DirectiveT extends DirectiveMeta> implements BoundTarget<Dir
|
|||
return null;
|
||||
}
|
||||
|
||||
isDeferred(element: Element | Component): boolean {
|
||||
isDeferred(element: Element): boolean {
|
||||
for (const block of this.deferredBlocks) {
|
||||
if (!this.deferredScopes.has(block)) {
|
||||
continue;
|
||||
|
|
@ -1273,6 +1296,14 @@ class R3BoundTarget<DirectiveT extends DirectiveMeta> implements BoundTarget<Dir
|
|||
return false;
|
||||
}
|
||||
|
||||
getOwnedDirectives(node: Component | Directive): DirectiveT[] | null {
|
||||
return this.ownedDirectives.get(node) || null;
|
||||
}
|
||||
|
||||
referencedDirectiveExists(name: string): boolean {
|
||||
return !this.missingDirectives.has(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds an entity with a specific name in a scope.
|
||||
* @param rootNode Root node of the scope.
|
||||
|
|
|
|||
|
|
@ -1032,7 +1032,7 @@ describe('t2 binding', () => {
|
|||
|
||||
function makeSelectorlessMatcher(
|
||||
directives: DirectiveMeta[],
|
||||
): SelectorlessMatcher<DirectiveMeta[]> {
|
||||
): SelectorlessMatcher<DirectiveMeta> {
|
||||
const registry = new Map<string, DirectiveMeta[]>();
|
||||
|
||||
for (const dir of directives) {
|
||||
|
|
@ -1282,5 +1282,70 @@ describe('t2 binding', () => {
|
|||
expect(res.getUsedDirectives().map((dir) => dir.name)).toEqual(['MyComp', 'Dir']);
|
||||
expect(res.getEagerlyUsedDirectives().map((dir) => dir.name)).toEqual(['MyComp', 'Dir']);
|
||||
});
|
||||
|
||||
it('should get the directives that are owned by a specific directive', () => {
|
||||
const template = parseTemplate('<MyComp @Dir/>', '', options);
|
||||
const registry = new Map<string, DirectiveMeta[]>([
|
||||
[
|
||||
'MyComp',
|
||||
[
|
||||
{
|
||||
...baseMeta,
|
||||
name: 'MyComp',
|
||||
isComponent: true,
|
||||
},
|
||||
{
|
||||
...baseMeta,
|
||||
name: 'CompHostDir',
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
'Dir',
|
||||
[
|
||||
{
|
||||
...baseMeta,
|
||||
name: 'Dir',
|
||||
},
|
||||
{
|
||||
...baseMeta,
|
||||
name: 'HostDir',
|
||||
},
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
const binder = new R3TargetBinder(new SelectorlessMatcher(registry));
|
||||
const res = binder.bind({template: template.nodes});
|
||||
const component = template.nodes[0] as a.Component;
|
||||
const directive = component.directives[0];
|
||||
expect(res.getOwnedDirectives(component)?.map((d) => d.name)).toEqual([
|
||||
'MyComp',
|
||||
'CompHostDir',
|
||||
]);
|
||||
expect(res.getOwnedDirectives(directive)?.map((d) => d.name)).toEqual(['Dir', 'HostDir']);
|
||||
});
|
||||
|
||||
it('should check whether a referenced directive exists', () => {
|
||||
const template = parseTemplate('<MyComp @MissingDir/><MissingComp @Dir/>', '', options);
|
||||
const binder = new R3TargetBinder(
|
||||
makeSelectorlessMatcher([
|
||||
{
|
||||
...baseMeta,
|
||||
name: 'MyComp',
|
||||
isComponent: true,
|
||||
},
|
||||
{
|
||||
...baseMeta,
|
||||
name: 'Dir',
|
||||
},
|
||||
]),
|
||||
);
|
||||
const res = binder.bind({template: template.nodes});
|
||||
expect(res.referencedDirectiveExists('MyComp')).toBe(true);
|
||||
expect(res.referencedDirectiveExists('Dir')).toBe(true);
|
||||
expect(res.referencedDirectiveExists('MissingDir')).toBe(false);
|
||||
expect(res.referencedDirectiveExists('MissingComp')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue