fix(ivy): handle method calls of local variables in template type checker (#33132)
Prior to this change, a method call of a local template variable would incorrectly be considered a call to a method on the component class. For example, this pattern would produce an error: ``` <ng-template let-method>{{ method(1) }}</ng-template> ``` Here, the method call should be targeting the `$implicit` variable on the template context, not the component class. This commit corrects the behavior by first resolving methods in the template before falling back on the component class. Fixes #32900 PR Close #33132
This commit is contained in:
parent
77240e1b60
commit
e2211ed211
|
@ -943,74 +943,24 @@ class TcbExpressionTranslator {
|
|||
*/
|
||||
protected resolve(ast: AST): ts.Expression|null {
|
||||
if (ast instanceof PropertyRead && ast.receiver instanceof ImplicitReceiver) {
|
||||
// Check whether the template metadata has bound a target for this expression. If so, then
|
||||
// resolve that target. If not, then the expression is referencing the top-level component
|
||||
// context.
|
||||
const binding = this.tcb.boundTarget.getExpressionTarget(ast);
|
||||
if (binding !== null) {
|
||||
// This expression has a binding to some variable or reference in the template. Resolve it.
|
||||
if (binding instanceof TmplAstVariable) {
|
||||
const expr = ts.getMutableClone(this.scope.resolve(binding));
|
||||
addParseSpanInfo(expr, toAbsoluteSpan(ast.span, this.sourceSpan));
|
||||
return expr;
|
||||
} else if (binding instanceof TmplAstReference) {
|
||||
if (!this.tcb.env.config.checkTypeOfReferences) {
|
||||
// References are pinned to 'any'.
|
||||
return NULL_AS_ANY;
|
||||
}
|
||||
|
||||
const target = this.tcb.boundTarget.getReferenceTarget(binding);
|
||||
if (target === null) {
|
||||
throw new Error(`Unbound reference? ${binding.name}`);
|
||||
}
|
||||
|
||||
// The reference is either to an element, an <ng-template> node, or to a directive on an
|
||||
// element or template.
|
||||
|
||||
if (target instanceof TmplAstElement) {
|
||||
const expr = ts.getMutableClone(this.scope.resolve(target));
|
||||
addParseSpanInfo(expr, toAbsoluteSpan(ast.span, this.sourceSpan));
|
||||
return expr;
|
||||
} else if (target instanceof TmplAstTemplate) {
|
||||
// Direct references to an <ng-template> node simply require a value of type
|
||||
// `TemplateRef<any>`. To get this, an expression of the form
|
||||
// `(null as any as TemplateRef<any>)` is constructed.
|
||||
let value: ts.Expression = ts.createNull();
|
||||
value =
|
||||
ts.createAsExpression(value, ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword));
|
||||
value = ts.createAsExpression(
|
||||
value,
|
||||
this.tcb.env.referenceExternalType('@angular/core', 'TemplateRef', [DYNAMIC_TYPE]));
|
||||
value = ts.createParen(value);
|
||||
addParseSpanInfo(value, toAbsoluteSpan(ast.span, this.sourceSpan));
|
||||
return value;
|
||||
} else {
|
||||
const expr = ts.getMutableClone(this.scope.resolve(target.node, target.directive));
|
||||
addParseSpanInfo(expr, toAbsoluteSpan(ast.span, this.sourceSpan));
|
||||
return expr;
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Unreachable: ${binding}`);
|
||||
}
|
||||
} else {
|
||||
// This is a PropertyRead(ImplicitReceiver) and probably refers to a property access on the
|
||||
// component context. Let it fall through resolution here so it will be caught when the
|
||||
// ImplicitReceiver is resolved in the branch below.
|
||||
return null;
|
||||
}
|
||||
// Try to resolve a bound target for this expression. If no such target is available, then
|
||||
// the expression is referencing the top-level component context. In that case, `null` is
|
||||
// returned here to let it fall through resolution so it will be caught when the
|
||||
// `ImplicitReceiver` is resolved in the branch below.
|
||||
return this.resolveTarget(ast);
|
||||
} else if (ast instanceof ImplicitReceiver) {
|
||||
// AST instances representing variables and references look very similar to property reads
|
||||
// from the component context: both have the shape
|
||||
// PropertyRead(ImplicitReceiver, 'propertyName').
|
||||
// or method calls from the component context: both have the shape
|
||||
// PropertyRead(ImplicitReceiver, 'propName') or MethodCall(ImplicitReceiver, 'methodName').
|
||||
//
|
||||
// `tcbExpression` will first try to `tcbResolve` the outer PropertyRead. If this works, it's
|
||||
// because the `BoundTarget` found an expression target for the whole expression, and
|
||||
// therefore `tcbExpression` will never attempt to `tcbResolve` the ImplicitReceiver of that
|
||||
// PropertyRead.
|
||||
// `translate` will first try to `resolve` the outer PropertyRead/MethodCall. If this works,
|
||||
// it's because the `BoundTarget` found an expression target for the whole expression, and
|
||||
// therefore `translate` will never attempt to `resolve` the ImplicitReceiver of that
|
||||
// PropertyRead/MethodCall.
|
||||
//
|
||||
// Therefore if `tcbResolve` is called on an `ImplicitReceiver`, it's because no outer
|
||||
// PropertyRead resolved to a variable or reference, and therefore this is a property read on
|
||||
// the component context itself.
|
||||
// Therefore if `resolve` is called on an `ImplicitReceiver`, it's because no outer
|
||||
// PropertyRead/MethodCall resolved to a variable or reference, and therefore this is a
|
||||
// property read or method call on the component context itself.
|
||||
return ts.createIdentifier('ctx');
|
||||
} else if (ast instanceof BindingPipe) {
|
||||
const expr = this.translate(ast.exp);
|
||||
|
@ -1025,20 +975,92 @@ class TcbExpressionTranslator {
|
|||
const result = tsCallMethod(pipe, 'transform', [expr, ...args]);
|
||||
addParseSpanInfo(result, toAbsoluteSpan(ast.span, this.sourceSpan));
|
||||
return result;
|
||||
} else if (
|
||||
ast instanceof MethodCall && ast.receiver instanceof ImplicitReceiver &&
|
||||
ast.name === '$any' && ast.args.length === 1) {
|
||||
const expr = this.translate(ast.args[0]);
|
||||
const exprAsAny =
|
||||
ts.createAsExpression(expr, ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword));
|
||||
const result = ts.createParen(exprAsAny);
|
||||
addParseSpanInfo(result, toAbsoluteSpan(ast.span, this.sourceSpan));
|
||||
return result;
|
||||
} else if (ast instanceof MethodCall && ast.receiver instanceof ImplicitReceiver) {
|
||||
// Resolve the special `$any(expr)` syntax to insert a cast of the argument to type `any`.
|
||||
if (ast.name === '$any' && ast.args.length === 1) {
|
||||
const expr = this.translate(ast.args[0]);
|
||||
const exprAsAny =
|
||||
ts.createAsExpression(expr, ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword));
|
||||
const result = ts.createParen(exprAsAny);
|
||||
addParseSpanInfo(result, toAbsoluteSpan(ast.span, this.sourceSpan));
|
||||
return result;
|
||||
}
|
||||
|
||||
// Attempt to resolve a bound target for the method, and generate the method call if a target
|
||||
// could be resolved. If no target is available, then the method is referencing the top-level
|
||||
// component context, in which case `null` is returned to let the `ImplicitReceiver` being
|
||||
// resolved to the component context.
|
||||
const receiver = this.resolveTarget(ast);
|
||||
if (receiver === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const method = ts.createPropertyAccess(wrapForDiagnostics(receiver), ast.name);
|
||||
const args = ast.args.map(arg => this.translate(arg));
|
||||
const node = ts.createCall(method, undefined, args);
|
||||
addParseSpanInfo(node, toAbsoluteSpan(ast.span, this.sourceSpan));
|
||||
return node;
|
||||
} else {
|
||||
// This AST isn't special after all.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to resolve a bound target for a given expression, and translates it into the
|
||||
* appropriate `ts.Expression` that represents the bound target. If no target is available,
|
||||
* `null` is returned.
|
||||
*/
|
||||
protected resolveTarget(ast: AST): ts.Expression|null {
|
||||
const binding = this.tcb.boundTarget.getExpressionTarget(ast);
|
||||
if (binding === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// This expression has a binding to some variable or reference in the template. Resolve it.
|
||||
if (binding instanceof TmplAstVariable) {
|
||||
const expr = ts.getMutableClone(this.scope.resolve(binding));
|
||||
addParseSpanInfo(expr, toAbsoluteSpan(ast.span, this.sourceSpan));
|
||||
return expr;
|
||||
} else if (binding instanceof TmplAstReference) {
|
||||
if (!this.tcb.env.config.checkTypeOfReferences) {
|
||||
// References are pinned to 'any'.
|
||||
return NULL_AS_ANY;
|
||||
}
|
||||
|
||||
const target = this.tcb.boundTarget.getReferenceTarget(binding);
|
||||
if (target === null) {
|
||||
throw new Error(`Unbound reference? ${binding.name}`);
|
||||
}
|
||||
|
||||
// The reference is either to an element, an <ng-template> node, or to a directive on an
|
||||
// element or template.
|
||||
|
||||
if (target instanceof TmplAstElement) {
|
||||
const expr = ts.getMutableClone(this.scope.resolve(target));
|
||||
addParseSpanInfo(expr, toAbsoluteSpan(ast.span, this.sourceSpan));
|
||||
return expr;
|
||||
} else if (target instanceof TmplAstTemplate) {
|
||||
// Direct references to an <ng-template> node simply require a value of type
|
||||
// `TemplateRef<any>`. To get this, an expression of the form
|
||||
// `(null as any as TemplateRef<any>)` is constructed.
|
||||
let value: ts.Expression = ts.createNull();
|
||||
value = ts.createAsExpression(value, ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword));
|
||||
value = ts.createAsExpression(
|
||||
value,
|
||||
this.tcb.env.referenceExternalType('@angular/core', 'TemplateRef', [DYNAMIC_TYPE]));
|
||||
value = ts.createParen(value);
|
||||
addParseSpanInfo(value, toAbsoluteSpan(ast.span, this.sourceSpan));
|
||||
return value;
|
||||
} else {
|
||||
const expr = ts.getMutableClone(this.scope.resolve(target.node, target.directive));
|
||||
addParseSpanInfo(expr, toAbsoluteSpan(ast.span, this.sourceSpan));
|
||||
return expr;
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Unreachable: ${binding}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -59,6 +59,12 @@ describe('type check blocks diagnostics', () => {
|
|||
.toContain('(ctx).method((ctx).a /*10,11*/, (ctx).b /*13,14*/) /*3,16*/;');
|
||||
});
|
||||
|
||||
it('should annotate method calls of variables', () => {
|
||||
const TEMPLATE = `<ng-template let-method>{{ method(a, b) }}</ng-template>`;
|
||||
expect(tcbWithSpans(TEMPLATE))
|
||||
.toContain('(_t2 /*27,40*/).method((ctx).a /*34,35*/, (ctx).b /*37,38*/) /*27,40*/;');
|
||||
});
|
||||
|
||||
it('should annotate function calls', () => {
|
||||
const TEMPLATE = `{{ method(a)(b, c) }}`;
|
||||
expect(tcbWithSpans(TEMPLATE))
|
||||
|
|
|
@ -73,6 +73,12 @@ describe('type check blocks', () => {
|
|||
expect(tcb(TEMPLATE)).toContain('var _t2 = _t1.$implicit;');
|
||||
});
|
||||
|
||||
it('should handle method calls of template variables', () => {
|
||||
const TEMPLATE = `<ng-template let-a>{{a(1)}}</ng-template>`;
|
||||
expect(tcb(TEMPLATE)).toContain('var _t2 = _t1.$implicit;');
|
||||
expect(tcb(TEMPLATE)).toContain('(_t2).a(1);');
|
||||
});
|
||||
|
||||
it('should handle implicit vars when using microsyntax', () => {
|
||||
const TEMPLATE = `<div *ngFor="let user of users"></div>`;
|
||||
expect(tcb(TEMPLATE)).toContain('var _t2 = _t1.$implicit;');
|
||||
|
|
Loading…
Reference in New Issue