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 {
|
protected resolve(ast: AST): ts.Expression|null {
|
||||||
if (ast instanceof PropertyRead && ast.receiver instanceof ImplicitReceiver) {
|
if (ast instanceof PropertyRead && ast.receiver instanceof ImplicitReceiver) {
|
||||||
// Check whether the template metadata has bound a target for this expression. If so, then
|
// Try to resolve a bound target for this expression. If no such target is available, then
|
||||||
// resolve that target. If not, then the expression is referencing the top-level component
|
// the expression is referencing the top-level component context. In that case, `null` is
|
||||||
// context.
|
// returned here to let it fall through resolution so it will be caught when the
|
||||||
const binding = this.tcb.boundTarget.getExpressionTarget(ast);
|
// `ImplicitReceiver` is resolved in the branch below.
|
||||||
if (binding !== null) {
|
return this.resolveTarget(ast);
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
} else if (ast instanceof ImplicitReceiver) {
|
} else if (ast instanceof ImplicitReceiver) {
|
||||||
// AST instances representing variables and references look very similar to property reads
|
// AST instances representing variables and references look very similar to property reads
|
||||||
// from the component context: both have the shape
|
// or method calls from the component context: both have the shape
|
||||||
// PropertyRead(ImplicitReceiver, 'propertyName').
|
// PropertyRead(ImplicitReceiver, 'propName') or MethodCall(ImplicitReceiver, 'methodName').
|
||||||
//
|
//
|
||||||
// `tcbExpression` will first try to `tcbResolve` the outer PropertyRead. If this works, it's
|
// `translate` will first try to `resolve` the outer PropertyRead/MethodCall. If this works,
|
||||||
// because the `BoundTarget` found an expression target for the whole expression, and
|
// 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
|
// therefore `translate` will never attempt to `resolve` the ImplicitReceiver of that
|
||||||
// PropertyRead.
|
// PropertyRead/MethodCall.
|
||||||
//
|
//
|
||||||
// Therefore if `tcbResolve` is called on an `ImplicitReceiver`, it's because no outer
|
// Therefore if `resolve` 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
|
// PropertyRead/MethodCall resolved to a variable or reference, and therefore this is a
|
||||||
// the component context itself.
|
// property read or method call on the component context itself.
|
||||||
return ts.createIdentifier('ctx');
|
return ts.createIdentifier('ctx');
|
||||||
} else if (ast instanceof BindingPipe) {
|
} else if (ast instanceof BindingPipe) {
|
||||||
const expr = this.translate(ast.exp);
|
const expr = this.translate(ast.exp);
|
||||||
|
@ -1025,20 +975,92 @@ class TcbExpressionTranslator {
|
||||||
const result = tsCallMethod(pipe, 'transform', [expr, ...args]);
|
const result = tsCallMethod(pipe, 'transform', [expr, ...args]);
|
||||||
addParseSpanInfo(result, toAbsoluteSpan(ast.span, this.sourceSpan));
|
addParseSpanInfo(result, toAbsoluteSpan(ast.span, this.sourceSpan));
|
||||||
return result;
|
return result;
|
||||||
} else if (
|
} else if (ast instanceof MethodCall && ast.receiver instanceof ImplicitReceiver) {
|
||||||
ast instanceof MethodCall && ast.receiver instanceof ImplicitReceiver &&
|
// Resolve the special `$any(expr)` syntax to insert a cast of the argument to type `any`.
|
||||||
ast.name === '$any' && ast.args.length === 1) {
|
if (ast.name === '$any' && ast.args.length === 1) {
|
||||||
const expr = this.translate(ast.args[0]);
|
const expr = this.translate(ast.args[0]);
|
||||||
const exprAsAny =
|
const exprAsAny =
|
||||||
ts.createAsExpression(expr, ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword));
|
ts.createAsExpression(expr, ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword));
|
||||||
const result = ts.createParen(exprAsAny);
|
const result = ts.createParen(exprAsAny);
|
||||||
addParseSpanInfo(result, toAbsoluteSpan(ast.span, this.sourceSpan));
|
addParseSpanInfo(result, toAbsoluteSpan(ast.span, this.sourceSpan));
|
||||||
return result;
|
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 {
|
} else {
|
||||||
// This AST isn't special after all.
|
// This AST isn't special after all.
|
||||||
return null;
|
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*/;');
|
.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', () => {
|
it('should annotate function calls', () => {
|
||||||
const TEMPLATE = `{{ method(a)(b, c) }}`;
|
const TEMPLATE = `{{ method(a)(b, c) }}`;
|
||||||
expect(tcbWithSpans(TEMPLATE))
|
expect(tcbWithSpans(TEMPLATE))
|
||||||
|
|
|
@ -73,6 +73,12 @@ describe('type check blocks', () => {
|
||||||
expect(tcb(TEMPLATE)).toContain('var _t2 = _t1.$implicit;');
|
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', () => {
|
it('should handle implicit vars when using microsyntax', () => {
|
||||||
const TEMPLATE = `<div *ngFor="let user of users"></div>`;
|
const TEMPLATE = `<div *ngFor="let user of users"></div>`;
|
||||||
expect(tcb(TEMPLATE)).toContain('var _t2 = _t1.$implicit;');
|
expect(tcb(TEMPLATE)).toContain('var _t2 = _t1.$implicit;');
|
||||||
|
|
Loading…
Reference in New Issue