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:
JoostK 2019-10-13 20:42:56 +02:00 committed by Andrew Kushnir
parent 77240e1b60
commit e2211ed211
3 changed files with 107 additions and 73 deletions

View File

@ -943,11 +943,80 @@ 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.
// 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
// or method calls from the component context: both have the shape
// PropertyRead(ImplicitReceiver, 'propName') or MethodCall(ImplicitReceiver, 'methodName').
//
// `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 `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);
let pipe: ts.Expression;
if (this.tcb.env.config.checkTypeOfPipes) {
pipe = this.tcb.getPipeByName(ast.name);
} else {
pipe = ts.createParen(ts.createAsExpression(
ts.createNull(), ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)));
}
const args = ast.args.map(arg => this.translate(arg));
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) {
// 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) {
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));
@ -976,8 +1045,7 @@ class TcbExpressionTranslator {
// `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, ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword));
value = ts.createAsExpression(
value,
this.tcb.env.referenceExternalType('@angular/core', 'TemplateRef', [DYNAMIC_TYPE]));
@ -992,52 +1060,6 @@ class TcbExpressionTranslator {
} 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;
}
} 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').
//
// `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.
//
// 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.
return ts.createIdentifier('ctx');
} else if (ast instanceof BindingPipe) {
const expr = this.translate(ast.exp);
let pipe: ts.Expression;
if (this.tcb.env.config.checkTypeOfPipes) {
pipe = this.tcb.getPipeByName(ast.name);
} else {
pipe = ts.createParen(ts.createAsExpression(
ts.createNull(), ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)));
}
const args = ast.args.map(arg => this.translate(arg));
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 {
// This AST isn't special after all.
return null;
}
}
}

View File

@ -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))

View File

@ -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;');