refactor(compiler): add getEntitiesInTemplateScope to template binder (#39048)

The template binding API in @angular/compiler exposes information about a
template that is synthesized from the template structure and its scope
(associated directives and pipes).

This commit introduces a new API, `getEntitiesInTemplateScope`, which
accepts a `Template` object (or `null` to indicate the root template) and
returns all `Reference` and `Variable` nodes that are visible at that level
of the template, including those declared in parent templates.

This API is needed by the template type-checker to support autocompletion
APIs for the Language Service.

PR Close #39048
This commit is contained in:
Alex Rickabaugh 2020-09-29 13:44:39 -04:00 committed by Joey Perrott
parent 3975dd90a6
commit 72755eadd2
2 changed files with 65 additions and 7 deletions

View File

@ -149,6 +149,12 @@ export interface BoundTarget<DirectiveT extends DirectiveMeta> {
*/
getNestingLevel(template: Template): number;
/**
* Get all `Reference`s and `Variables` visible within the given `Template` (or at the top level,
* if `null` is passed).
*/
getEntitiesInTemplateScope(template: Template|null): ReadonlySet<Reference|Variable>;
/**
* Get a list of all the directives used by the target.
*/

View File

@ -37,6 +37,10 @@ export class R3TargetBinder<DirectiveT extends DirectiveMeta> implements TargetB
// scopes in the template and makes them available for later use.
const scope = Scope.apply(target.template);
// Use the `Scope` to extract the entities present at every level of the template.
const templateEntities = extractTemplateEntities(scope);
// Next, perform directive matching on the template using the `DirectiveBinder`. This returns:
// - directives: Map of nodes (elements & ng-templates) to the directives on them.
// - bindings: Map of inputs, outputs, and attributes to the directive/element that claims
@ -49,7 +53,8 @@ export class R3TargetBinder<DirectiveT extends DirectiveMeta> implements TargetB
const {expressions, symbols, nestingLevel, usedPipes} =
TemplateBinder.apply(target.template, scope);
return new R3BoundTarget(
target, directives, bindings, references, expressions, symbols, nestingLevel, usedPipes);
target, directives, bindings, references, expressions, symbols, nestingLevel,
templateEntities, usedPipes);
}
}
@ -71,14 +76,18 @@ class Scope implements Visitor {
*/
readonly childScopes = new Map<Template, Scope>();
private constructor(readonly parentScope?: Scope) {}
private constructor(readonly parentScope: Scope|null, readonly template: Template|null) {}
static newRootScope(): Scope {
return new Scope(null, null);
}
/**
* Process a template (either as a `Template` sub-template with variables, or a plain array of
* template `Node`s) and construct its `Scope`.
*/
static apply(template: Template|Node[]): Scope {
const scope = new Scope();
static apply(template: Node[]): Scope {
const scope = Scope.newRootScope();
scope.ingest(template);
return scope;
}
@ -113,7 +122,7 @@ class Scope implements Visitor {
template.references.forEach(node => this.visitReference(node));
// Next, create an inner scope and process the template within it.
const scope = new Scope(this);
const scope = new Scope(this, template);
scope.ingest(template);
this.childScopes.set(template, scope);
}
@ -153,7 +162,7 @@ class Scope implements Visitor {
if (this.namedEntities.has(name)) {
// Found in the local scope.
return this.namedEntities.get(name)!;
} else if (this.parentScope !== undefined) {
} else if (this.parentScope !== null) {
// Not in the local scope, but there's a parent scope so check there.
return this.parentScope.lookup(name);
} else {
@ -517,7 +526,13 @@ export class R3BoundTarget<DirectiveT extends DirectiveMeta> implements BoundTar
{directive: DirectiveT, node: Element|Template}|Element|Template>,
private exprTargets: Map<AST, Reference|Variable>,
private symbols: Map<Reference|Variable, Template>,
private nestingLevel: Map<Template, number>, private usedPipes: Set<string>) {}
private nestingLevel: Map<Template, number>,
private templateEntities: Map<Template|null, ReadonlySet<Reference|Variable>>,
private usedPipes: Set<string>) {}
getEntitiesInTemplateScope(template: Template|null): ReadonlySet<Reference|Variable> {
return this.templateEntities.get(template) ?? new Set();
}
getDirectivesOfNode(node: Element|Template): DirectiveT[]|null {
return this.directives.get(node) || null;
@ -555,3 +570,40 @@ export class R3BoundTarget<DirectiveT extends DirectiveMeta> implements BoundTar
return Array.from(this.usedPipes);
}
}
function extractTemplateEntities(rootScope: Scope): Map<Template|null, Set<Reference|Variable>> {
const entityMap = new Map<Template|null, Map<string, Reference|Variable>>();
function extractScopeEntities(scope: Scope): Map<string, Reference|Variable> {
if (entityMap.has(scope.template)) {
return entityMap.get(scope.template)!;
}
const currentEntities = scope.namedEntities;
let templateEntities: Map<string, Reference|Variable>;
if (scope.parentScope !== null) {
templateEntities = new Map([...extractScopeEntities(scope.parentScope), ...currentEntities]);
} else {
templateEntities = new Map(currentEntities);
}
entityMap.set(scope.template, templateEntities);
return templateEntities;
}
const scopesToProcess: Scope[] = [rootScope];
while (scopesToProcess.length > 0) {
const scope = scopesToProcess.pop()!;
for (const childScope of scope.childScopes.values()) {
scopesToProcess.push(childScope);
}
extractScopeEntities(scope);
}
const templateEntities = new Map<Template|null, Set<Reference|Variable>>();
for (const [template, entities] of entityMap) {
templateEntities.set(template, new Set(entities.values()));
}
return templateEntities;
}