feat(compiler): add name spans for property reads and method calls (#36826)

ASTs for property read and method calls contain information about
the entire span of the expression, including its receiver. Use cases
like a language service and compile error messages may be more
interested in the span of the direct identifier for which the
expression is constructed (i.e. an accessed property). To support this,
this commit adds a `nameSpan` property on

- `PropertyRead`s
- `SafePropertyRead`s
- `PropertyWrite`s
- `MethodCall`s
- `SafeMethodCall`s

The `nameSpan` property already existed for `BindingPipe`s.

This commit also updates usages of these expressions' `sourceSpan`s in
Ngtsc and the langauge service to use `nameSpan`s where appropriate.

PR Close #36826
This commit is contained in:
Ayaz Hafiz
2020-04-27 18:54:30 -07:00
committed by Misko Hevery
parent 1142c378fd
commit eb34aa551a
20 changed files with 496 additions and 199 deletions

View File

@ -10,7 +10,7 @@ import {AST, AstVisitor, ASTWithSource, Binary, BindingPipe, Chain, Conditional,
import * as ts from 'typescript';
import {TypeCheckingConfig} from './api';
import {addParseSpanInfo, ignoreDiagnostics, wrapForDiagnostics} from './diagnostics';
import {addParseSpanInfo, wrapForDiagnostics} from './diagnostics';
import {tsCastToAny} from './ts_util';
export const NULL_AS_ANY =
@ -179,6 +179,7 @@ class AstTranslator implements AstVisitor {
visitMethodCall(ast: MethodCall): ts.Expression {
const receiver = wrapForDiagnostics(this.translate(ast.receiver));
const method = ts.createPropertyAccess(receiver, ast.name);
addParseSpanInfo(method, ast.nameSpan);
const args = ast.args.map(expr => this.translate(expr));
const node = ts.createCall(method, undefined, args);
addParseSpanInfo(node, ast.sourceSpan);
@ -207,7 +208,9 @@ class AstTranslator implements AstVisitor {
// This is a normal property read - convert the receiver to an expression and emit the correct
// TypeScript expression to read the property.
const receiver = wrapForDiagnostics(this.translate(ast.receiver));
const node = ts.createPropertyAccess(receiver, ast.name);
const name = ts.createPropertyAccess(receiver, ast.name);
addParseSpanInfo(name, ast.nameSpan);
const node = wrapForDiagnostics(name);
addParseSpanInfo(node, ast.sourceSpan);
return node;
}
@ -215,10 +218,18 @@ class AstTranslator implements AstVisitor {
visitPropertyWrite(ast: PropertyWrite): ts.Expression {
const receiver = wrapForDiagnostics(this.translate(ast.receiver));
const left = ts.createPropertyAccess(receiver, ast.name);
// TODO(joost): annotate `left` with the span of the property access, which is not currently
// available on `ast`.
addParseSpanInfo(left, ast.nameSpan);
// TypeScript reports assignment errors on the entire lvalue expression. Annotate the lvalue of
// the assignment with the sourceSpan, which includes receivers, rather than nameSpan for
// consistency of the diagnostic location.
// a.b.c = 1
// ^^^^^^^^^ sourceSpan
// ^ nameSpan
const leftWithPath = wrapForDiagnostics(left);
addParseSpanInfo(leftWithPath, ast.sourceSpan);
const right = this.translate(ast.value);
const node = wrapForDiagnostics(ts.createBinary(left, ts.SyntaxKind.EqualsToken, right));
const node =
wrapForDiagnostics(ts.createBinary(leftWithPath, ts.SyntaxKind.EqualsToken, right));
addParseSpanInfo(node, ast.sourceSpan);
return node;
}
@ -235,15 +246,18 @@ class AstTranslator implements AstVisitor {
if (this.config.strictSafeNavigationTypes) {
// "a?.method(...)" becomes (null as any ? a!.method(...) : undefined)
const method = ts.createPropertyAccess(ts.createNonNullExpression(receiver), ast.name);
addParseSpanInfo(method, ast.nameSpan);
const call = ts.createCall(method, undefined, args);
node = ts.createParen(ts.createConditional(NULL_AS_ANY, call, UNDEFINED));
} else if (VeSafeLhsInferenceBugDetector.veWillInferAnyFor(ast)) {
// "a?.method(...)" becomes (a as any).method(...)
const method = ts.createPropertyAccess(tsCastToAny(receiver), ast.name);
addParseSpanInfo(method, ast.nameSpan);
node = ts.createCall(method, undefined, args);
} else {
// "a?.method(...)" becomes (a!.method(...) as any)
const method = ts.createPropertyAccess(ts.createNonNullExpression(receiver), ast.name);
addParseSpanInfo(method, ast.nameSpan);
node = tsCastToAny(ts.createCall(method, undefined, args));
}
addParseSpanInfo(node, ast.sourceSpan);

View File

@ -1125,6 +1125,7 @@ class TcbExpressionTranslator {
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`.
// `$any(expr)` -> `expr as any`
if (ast.name === '$any' && ast.args.length === 1) {
const expr = this.translate(ast.args[0]);
const exprAsAny =
@ -1144,6 +1145,7 @@ class TcbExpressionTranslator {
}
const method = ts.createPropertyAccess(wrapForDiagnostics(receiver), ast.name);
addParseSpanInfo(method, ast.nameSpan);
const args = ast.args.map(arg => this.translate(arg));
const node = ts.createCall(method, undefined, args);
addParseSpanInfo(node, ast.sourceSpan);
@ -1435,7 +1437,7 @@ class TcbEventHandlerTranslator extends TcbExpressionTranslator {
if (ast instanceof PropertyRead && ast.receiver instanceof ImplicitReceiver &&
ast.name === EVENT_PARAMETER) {
const event = ts.createIdentifier(EVENT_PARAMETER);
addParseSpanInfo(event, ast.sourceSpan);
addParseSpanInfo(event, ast.nameSpan);
return event;
}

View File

@ -130,7 +130,7 @@ runInEachFileSystem(() => {
[ngForDeclaration()], [ngForDts()]);
expect(messages).toEqual([
`synthetic.html(1, 40): Property 'namme' does not exist on type '{ name: string; }'. Did you mean 'name'?`,
`synthetic.html(1, 47): Property 'namme' does not exist on type '{ name: string; }'. Did you mean 'name'?`,
]);
});
@ -329,7 +329,7 @@ runInEachFileSystem(() => {
};
}`);
expect(messages).toEqual([`synthetic.html(1, 26): Object is possibly 'undefined'.`]);
expect(messages).toEqual([`synthetic.html(1, 41): Object is possibly 'undefined'.`]);
});
it('does not produce diagnostic for checked property access', () => {
@ -367,6 +367,85 @@ class TestComponent {
]);
});
});
describe('method call spans', () => {
it('reports invalid method name on method name span', () => {
const messages = diagnose(`{{ person.getNName() }}`, `
export class TestComponent {
person: {
getName(): string;
};
}`);
expect(messages).toEqual([
`synthetic.html(1, 11): Property 'getNName' does not exist on type '{ getName(): string; }'. Did you mean 'getName'?`
]);
});
it('reports invalid method call signature on parameter span', () => {
const messages = diagnose(`{{ person.getName('abcd') }}`, `
export class TestComponent {
person: {
getName(): string;
};
}`);
expect(messages).toEqual([`synthetic.html(1, 19): Expected 0 arguments, but got 1.`]);
});
});
describe('safe method call spans', () => {
it('reports invalid method name on method name span', () => {
const messages = diagnose(`{{ person?.getNName() }}`, `
export class TestComponent {
person?: {
getName(): string;
};
}`);
expect(messages).toEqual([
`synthetic.html(1, 12): Property 'getNName' does not exist on type '{ getName(): string; }'. Did you mean 'getName'?`
]);
});
it('reports invalid method call signature on parameter span', () => {
const messages = diagnose(`{{ person?.getName('abcd') }}`, `
export class TestComponent {
person?: {
getName(): string;
};
}`);
expect(messages).toEqual([`synthetic.html(1, 20): Expected 0 arguments, but got 1.`]);
});
});
describe('property write spans', () => {
it('reports invalid receiver property access on property access name span', () => {
const messages = diagnose(`<div (click)="person.nname = 'jacky'"></div>`, `
export class TestComponent {
person: {
name: string;
};
}`);
expect(messages).toEqual([
`synthetic.html(1, 22): Property 'nname' does not exist on type '{ name: string; }'. Did you mean 'name'?`
]);
});
it('reports unassignable value on property write span', () => {
const messages = diagnose(`<div (click)="person.name = 2"></div>`, `
export class TestComponent {
person: {
name: string;
};
}`);
expect(messages).toEqual(
[`synthetic.html(1, 15): Type '2' is not assignable to type 'string'.`]);
});
});
});
function diagnose(

View File

@ -12,17 +12,18 @@ describe('type check blocks diagnostics', () => {
describe('parse spans', () => {
it('should annotate binary ops', () => {
expect(tcbWithSpans('{{ a + b }}'))
.toContain('"" + (((ctx).a /*3,4*/) + ((ctx).b /*7,8*/) /*3,8*/);');
.toContain('(((ctx).a /*3,4*/) /*3,4*/) + (((ctx).b /*7,8*/) /*7,8*/) /*3,8*/');
});
it('should annotate conditions', () => {
expect(tcbWithSpans('{{ a ? b : c }}'))
.toContain('((ctx).a /*3,4*/ ? (ctx).b /*7,8*/ : (ctx).c /*11,12*/) /*3,12*/;');
.toContain(
'(((ctx).a /*3,4*/) /*3,4*/ ? ((ctx).b /*7,8*/) /*7,8*/ : ((ctx).c /*11,12*/) /*11,12*/) /*3,12*/');
});
it('should annotate interpolations', () => {
expect(tcbWithSpans('{{ hello }} {{ world }}'))
.toContain('"" + (ctx).hello /*3,8*/ + (ctx).world /*15,20*/;');
.toContain('"" + ((ctx).hello /*3,8*/) /*3,8*/ + ((ctx).world /*15,20*/) /*15,20*/');
});
it('should annotate literal map expressions', () => {
@ -30,12 +31,14 @@ describe('type check blocks diagnostics', () => {
// statement, which would wrap it into parenthesis that clutter the expected output.
const TEMPLATE = '{{ m({foo: a, bar: b}) }}';
expect(tcbWithSpans(TEMPLATE))
.toContain('m({ "foo": (ctx).a /*11,12*/, "bar": (ctx).b /*19,20*/ } /*5,21*/)');
.toContain(
'(ctx).m /*3,4*/({ "foo": ((ctx).a /*11,12*/) /*11,12*/, "bar": ((ctx).b /*19,20*/) /*19,20*/ } /*5,21*/) /*3,22*/');
});
it('should annotate literal array expressions', () => {
const TEMPLATE = '{{ [a, b] }}';
expect(tcbWithSpans(TEMPLATE)).toContain('[(ctx).a /*4,5*/, (ctx).b /*7,8*/] /*3,9*/;');
expect(tcbWithSpans(TEMPLATE))
.toContain('[((ctx).a /*4,5*/) /*4,5*/, ((ctx).b /*7,8*/) /*7,8*/] /*3,9*/');
});
it('should annotate literals', () => {
@ -45,77 +48,84 @@ describe('type check blocks diagnostics', () => {
it('should annotate non-null assertions', () => {
const TEMPLATE = `{{ a! }}`;
expect(tcbWithSpans(TEMPLATE)).toContain('(((ctx).a /*3,4*/)! /*3,5*/);');
expect(tcbWithSpans(TEMPLATE)).toContain('(((ctx).a /*3,4*/) /*3,4*/)! /*3,5*/');
});
it('should annotate prefix not', () => {
const TEMPLATE = `{{ !a }}`;
expect(tcbWithSpans(TEMPLATE)).toContain('!((ctx).a /*4,5*/) /*3,5*/;');
expect(tcbWithSpans(TEMPLATE)).toContain('!(((ctx).a /*4,5*/) /*4,5*/) /*3,5*/;');
});
it('should annotate method calls', () => {
const TEMPLATE = `{{ method(a, b) }}`;
expect(tcbWithSpans(TEMPLATE))
.toContain('(ctx).method((ctx).a /*10,11*/, (ctx).b /*13,14*/) /*3,15*/;');
.toContain(
'(ctx).method /*3,9*/(((ctx).a /*10,11*/) /*10,11*/, ((ctx).b /*13,14*/) /*13,14*/) /*3,15*/');
});
it('should annotate method calls of variables', () => {
const TEMPLATE = `<ng-template let-method>{{ method(a, b) }}</ng-template>`;
expect(tcbWithSpans(TEMPLATE))
.toContain('(_t2 /*27,39*/).method((ctx).a /*34,35*/, (ctx).b /*37,38*/) /*27,39*/;');
.toContain(
'(_t2 /*27,39*/).method /*27,33*/(((ctx).a /*34,35*/) /*34,35*/, ((ctx).b /*37,38*/) /*37,38*/) /*27,39*/');
});
it('should annotate function calls', () => {
const TEMPLATE = `{{ method(a)(b, c) }}`;
expect(tcbWithSpans(TEMPLATE))
.toContain(
'((ctx).method((ctx).a /*10,11*/) /*3,12*/)((ctx).b /*13,14*/, (ctx).c /*16,17*/) /*3,18*/;');
'((ctx).method /*3,9*/(((ctx).a /*10,11*/) /*10,11*/) /*3,12*/)(((ctx).b /*13,14*/) /*13,14*/, ((ctx).c /*16,17*/) /*16,17*/) /*3,18*/');
});
it('should annotate property access', () => {
const TEMPLATE = `{{ a.b.c }}`;
expect(tcbWithSpans(TEMPLATE)).toContain('(((ctx).a /*3,4*/).b /*3,6*/).c /*3,8*/;');
expect(tcbWithSpans(TEMPLATE))
.toContain('((((((ctx).a /*3,4*/) /*3,4*/).b /*5,6*/) /*3,6*/).c /*7,8*/) /*3,8*/');
});
it('should annotate property writes', () => {
const TEMPLATE = `<div (click)="a.b.c = d"></div>`;
const TEMPLATE = `<div (click)='a.b.c = d'></div>`;
expect(tcbWithSpans(TEMPLATE))
.toContain('((((ctx).a /*14,15*/).b /*14,17*/).c = (ctx).d /*22,23*/) /*14,23*/');
.toContain(
'(((((((ctx).a /*14,15*/) /*14,15*/).b /*16,17*/) /*14,17*/).c /*18,19*/) /*14,23*/ = ((ctx).d /*22,23*/) /*22,23*/) /*14,23*/');
});
it('should annotate keyed property access', () => {
const TEMPLATE = `{{ a[b] }}`;
expect(tcbWithSpans(TEMPLATE)).toContain('((ctx).a /*3,4*/)[(ctx).b /*5,6*/] /*3,7*/;');
expect(tcbWithSpans(TEMPLATE))
.toContain('(((ctx).a /*3,4*/) /*3,4*/)[((ctx).b /*5,6*/) /*5,6*/] /*3,7*/');
});
it('should annotate keyed property writes', () => {
const TEMPLATE = `<div (click)="a[b] = c"></div>`;
expect(tcbWithSpans(TEMPLATE))
.toContain('(((ctx).a /*14,15*/)[(ctx).b /*16,17*/] = (ctx).c /*21,22*/) /*14,22*/');
.toContain(
'((((ctx).a /*14,15*/) /*14,15*/)[((ctx).b /*16,17*/) /*16,17*/] = ((ctx).c /*21,22*/) /*21,22*/) /*14,22*/');
});
it('should annotate safe property access', () => {
const TEMPLATE = `{{ a?.b }}`;
expect(tcbWithSpans(TEMPLATE))
.toContain('((null as any) ? ((ctx).a /*3,4*/)!.b : undefined) /*3,7*/');
.toContain('((null as any) ? (((ctx).a /*3,4*/) /*3,4*/)!.b : undefined) /*3,7*/');
});
it('should annotate safe method calls', () => {
const TEMPLATE = `{{ a?.method(b) }}`;
expect(tcbWithSpans(TEMPLATE))
.toContain(
'((null as any) ? ((ctx).a /*3,4*/)!.method((ctx).b /*13,14*/) : undefined) /*3,15*/');
'((null as any) ? (((ctx).a /*3,4*/) /*3,4*/)!.method /*6,12*/(((ctx).b /*13,14*/) /*13,14*/) : undefined) /*3,15*/');
});
it('should annotate $any casts', () => {
const TEMPLATE = `{{ $any(a) }}`;
expect(tcbWithSpans(TEMPLATE)).toContain('((ctx).a /*8,9*/ as any) /*3,10*/;');
expect(tcbWithSpans(TEMPLATE)).toContain('(((ctx).a /*8,9*/) /*8,9*/ as any) /*3,10*/');
});
it('should annotate chained expressions', () => {
const TEMPLATE = `<div (click)="a; b; c"></div>`;
const TEMPLATE = `<div (click)='a; b; c'></div>`;
expect(tcbWithSpans(TEMPLATE))
.toContain('((ctx).a /*14,15*/, (ctx).b /*17,18*/, (ctx).c /*20,21*/) /*14,21*/');
.toContain(
'(((ctx).a /*14,15*/) /*14,15*/, ((ctx).b /*17,18*/) /*17,18*/, ((ctx).c /*20,21*/) /*20,21*/) /*14,21*/');
});
it('should annotate pipe usages', () => {
@ -127,7 +137,7 @@ describe('type check blocks diagnostics', () => {
}];
const block = tcbWithSpans(TEMPLATE, PIPES);
expect(block).toContain(
'(null as TestPipe).transform((ctx).a /*3,4*/, (ctx).b /*12,13*/) /*3,13*/;');
'(null as TestPipe).transform(((ctx).a /*3,4*/) /*3,4*/, ((ctx).b /*12,13*/) /*12,13*/) /*3,13*/;');
});
describe('attaching multiple comments for multiple references', () => {
@ -136,7 +146,7 @@ describe('type check blocks diagnostics', () => {
expect(tcbWithSpans(TEMPLATE)).toContain('((_t1 /*19,20*/) || (_t1 /*24,25*/) /*19,25*/);');
});
it('should be correct for template vars', () => {
const TEMPLATE = `<ng-template let-a="b">{{ a || a }}</ng-template>`;
const TEMPLATE = `<ng-template let-a='b'>{{ a || a }}</ng-template>`;
expect(tcbWithSpans(TEMPLATE)).toContain('((_t2 /*26,27*/) || (_t2 /*31,32*/) /*26,32*/);');
});
it('should be correct for directive refs', () => {

View File

@ -13,32 +13,33 @@ import {ALL_ENABLED_CONFIG, tcb, TestDeclaration, TestDirective} from './test_ut
describe('type check blocks', () => {
it('should generate a basic block for a binding', () => {
expect(tcb('{{hello}} {{world}}')).toContain('"" + (ctx).hello + (ctx).world;');
expect(tcb('{{hello}} {{world}}')).toContain('"" + ((ctx).hello) + ((ctx).world);');
});
it('should generate literal map expressions', () => {
const TEMPLATE = '{{ method({foo: a, bar: b}) }}';
expect(tcb(TEMPLATE)).toContain('(ctx).method({ "foo": (ctx).a, "bar": (ctx).b });');
expect(tcb(TEMPLATE)).toContain('(ctx).method({ "foo": ((ctx).a), "bar": ((ctx).b) });');
});
it('should generate literal array expressions', () => {
const TEMPLATE = '{{ method([a, b]) }}';
expect(tcb(TEMPLATE)).toContain('(ctx).method([(ctx).a, (ctx).b]);');
expect(tcb(TEMPLATE)).toContain('(ctx).method([((ctx).a), ((ctx).b)]);');
});
it('should handle non-null assertions', () => {
const TEMPLATE = `{{a!}}`;
expect(tcb(TEMPLATE)).toContain('(((ctx).a)!);');
expect(tcb(TEMPLATE)).toContain('((((ctx).a))!);');
});
it('should handle keyed property access', () => {
const TEMPLATE = `{{a[b]}}`;
expect(tcb(TEMPLATE)).toContain('((ctx).a)[(ctx).b];');
expect(tcb(TEMPLATE)).toContain('(((ctx).a))[((ctx).b)];');
});
it('should handle nested ternary expressions', () => {
const TEMPLATE = `{{a ? b : c ? d : e}}`;
expect(tcb(TEMPLATE)).toContain('((ctx).a ? (ctx).b : ((ctx).c ? (ctx).d : (ctx).e))');
expect(tcb(TEMPLATE))
.toContain('(((ctx).a) ? ((ctx).b) : (((ctx).c) ? ((ctx).d) : ((ctx).e)))');
});
it('should handle attribute values for directive inputs', () => {
@ -115,7 +116,8 @@ describe('type check blocks', () => {
},
}];
expect(tcb(TEMPLATE, DIRECTIVES))
.toContain('var _t2 = Dir.ngTypeCtor({ "fieldA": ((ctx).foo), "fieldB": (null as any) });');
.toContain(
'var _t2 = Dir.ngTypeCtor({ "fieldA": (((ctx).foo)), "fieldB": (null as any) });');
});
it('should generate a forward element reference correctly', () => {
@ -123,7 +125,8 @@ describe('type check blocks', () => {
{{ i.value }}
<input #i>
`;
expect(tcb(TEMPLATE)).toContain('var _t1 = document.createElement("input"); "" + (_t1).value;');
expect(tcb(TEMPLATE))
.toContain('var _t1 = document.createElement("input"); "" + ((_t1).value);');
});
it('should generate a forward directive reference correctly', () => {
@ -139,7 +142,7 @@ describe('type check blocks', () => {
}];
expect(tcb(TEMPLATE, DIRECTIVES))
.toContain(
'var _t1 = Dir.ngTypeCtor({}); "" + (_t1).value; var _t2 = document.createElement("div");');
'var _t1 = Dir.ngTypeCtor({}); "" + ((_t1).value); var _t2 = document.createElement("div");');
});
it('should handle style and class bindings specially', () => {
@ -147,7 +150,7 @@ describe('type check blocks', () => {
<div [style]="a" [class]="b"></div>
`;
const block = tcb(TEMPLATE);
expect(block).toContain('(ctx).a; (ctx).b;');
expect(block).toContain('((ctx).a); ((ctx).b);');
// There should be no assignments to the class or style properties.
expect(block).not.toContain('.class = ');
@ -218,7 +221,7 @@ describe('type check blocks', () => {
it('should handle $any casts', () => {
const TEMPLATE = `{{$any(a)}}`;
const block = tcb(TEMPLATE);
expect(block).toContain('((ctx).a as any);');
expect(block).toContain('(((ctx).a) as any);');
});
describe('experimental DOM checking via lib.dom.d.ts', () => {
@ -244,7 +247,7 @@ describe('type check blocks', () => {
}];
const TEMPLATE = `<div *ngIf="person"></div>`;
const block = tcb(TEMPLATE, DIRECTIVES);
expect(block).toContain('if (NgIf.ngTemplateGuard_ngIf(_t1, (ctx).person))');
expect(block).toContain('if (NgIf.ngTemplateGuard_ngIf(_t1, ((ctx).person)))');
});
it('should emit binding guards', () => {
@ -260,7 +263,7 @@ describe('type check blocks', () => {
}];
const TEMPLATE = `<div *ngIf="person !== null"></div>`;
const block = tcb(TEMPLATE, DIRECTIVES);
expect(block).toContain('if (((ctx).person) !== (null))');
expect(block).toContain('if ((((ctx).person)) !== (null))');
});
});
@ -357,12 +360,12 @@ describe('type check blocks', () => {
it('should descend into template bodies when enabled', () => {
const block = tcb(TEMPLATE, DIRECTIVES);
expect(block).toContain('(ctx).a;');
expect(block).toContain('((ctx).a);');
});
it('should not descend into template bodies when disabled', () => {
const DISABLED_CONFIG: TypeCheckingConfig = {...BASE_CONFIG, checkTemplateBodies: false};
const block = tcb(TEMPLATE, DIRECTIVES, DISABLED_CONFIG);
expect(block).not.toContain('(ctx).a;');
expect(block).not.toContain('((ctx).a);');
});
});
@ -371,15 +374,15 @@ describe('type check blocks', () => {
it('should include null and undefined when enabled', () => {
const block = tcb(TEMPLATE, DIRECTIVES);
expect(block).toContain('Dir.ngTypeCtor({ "dirInput": ((ctx).a) })');
expect(block).toContain('(ctx).b;');
expect(block).toContain('Dir.ngTypeCtor({ "dirInput": (((ctx).a)) })');
expect(block).toContain('((ctx).b);');
});
it('should use the non-null assertion operator when disabled', () => {
const DISABLED_CONFIG:
TypeCheckingConfig = {...BASE_CONFIG, strictNullInputBindings: false};
const block = tcb(TEMPLATE, DIRECTIVES, DISABLED_CONFIG);
expect(block).toContain('Dir.ngTypeCtor({ "dirInput": ((ctx).a!) })');
expect(block).toContain('(ctx).b!;');
expect(block).toContain('Dir.ngTypeCtor({ "dirInput": (((ctx).a)!) })');
expect(block).toContain('((ctx).b)!;');
});
});
@ -387,8 +390,8 @@ describe('type check blocks', () => {
it('should check types of bindings when enabled', () => {
const TEMPLATE = `<div dir [dirInput]="a" [nonDirInput]="b"></div>`;
const block = tcb(TEMPLATE, DIRECTIVES);
expect(block).toContain('Dir.ngTypeCtor({ "dirInput": ((ctx).a) })');
expect(block).toContain('(ctx).b;');
expect(block).toContain('Dir.ngTypeCtor({ "dirInput": (((ctx).a)) })');
expect(block).toContain('((ctx).b);');
});
it('should not check types of bindings when disabled', () => {
@ -396,8 +399,8 @@ describe('type check blocks', () => {
const DISABLED_CONFIG:
TypeCheckingConfig = {...BASE_CONFIG, checkTypeOfInputBindings: false};
const block = tcb(TEMPLATE, DIRECTIVES, DISABLED_CONFIG);
expect(block).toContain('Dir.ngTypeCtor({ "dirInput": (((ctx).a as any)) })');
expect(block).toContain('((ctx).b as any);');
expect(block).toContain('Dir.ngTypeCtor({ "dirInput": ((((ctx).a) as any)) })');
expect(block).toContain('(((ctx).b) as any);');
});
it('should wrap the cast to any in parentheses when required', () => {
@ -406,7 +409,7 @@ describe('type check blocks', () => {
TypeCheckingConfig = {...BASE_CONFIG, checkTypeOfInputBindings: false};
const block = tcb(TEMPLATE, DIRECTIVES, DISABLED_CONFIG);
expect(block).toContain(
'Dir.ngTypeCtor({ "dirInput": (((((ctx).a) === ((ctx).b)) as any)) })');
'Dir.ngTypeCtor({ "dirInput": ((((((ctx).a)) === (((ctx).b))) as any)) })');
});
});
@ -550,12 +553,12 @@ describe('type check blocks', () => {
it('should check types of pipes when enabled', () => {
const block = tcb(TEMPLATE, PIPES);
expect(block).toContain('(null as TestPipe).transform((ctx).a, (ctx).b, (ctx).c);');
expect(block).toContain('(null as TestPipe).transform(((ctx).a), ((ctx).b), ((ctx).c));');
});
it('should not check types of pipes when disabled', () => {
const DISABLED_CONFIG: TypeCheckingConfig = {...BASE_CONFIG, checkTypeOfPipes: false};
const block = tcb(TEMPLATE, PIPES, DISABLED_CONFIG);
expect(block).toContain('(null as any).transform((ctx).a, (ctx).b, (ctx).c);');
expect(block).toContain('(null as any).transform(((ctx).a), ((ctx).b), ((ctx).c));');
});
});
@ -564,15 +567,15 @@ describe('type check blocks', () => {
it('should use undefined for safe navigation operations when enabled', () => {
const block = tcb(TEMPLATE, DIRECTIVES);
expect(block).toContain('((null as any) ? ((ctx).a)!.method() : undefined)');
expect(block).toContain('((null as any) ? ((ctx).a)!.b : undefined)');
expect(block).toContain('((null as any) ? (((ctx).a))!.method() : undefined)');
expect(block).toContain('((null as any) ? (((ctx).a))!.b : undefined)');
});
it('should use an \'any\' type for safe navigation operations when disabled', () => {
const DISABLED_CONFIG:
TypeCheckingConfig = {...BASE_CONFIG, strictSafeNavigationTypes: false};
const block = tcb(TEMPLATE, DIRECTIVES, DISABLED_CONFIG);
expect(block).toContain('(((ctx).a)!.method() as any)');
expect(block).toContain('(((ctx).a)!.b as any)');
expect(block).toContain('((((ctx).a))!.method() as any)');
expect(block).toContain('((((ctx).a))!.b as any)');
});
});
@ -580,14 +583,14 @@ describe('type check blocks', () => {
const TEMPLATE = `{{a.method()?.b}} {{a()?.method()}}`;
it('should check the presence of a property/method on the receiver when enabled', () => {
const block = tcb(TEMPLATE, DIRECTIVES);
expect(block).toContain('((null as any) ? (((ctx).a).method())!.b : undefined)');
expect(block).toContain('((null as any) ? ((((ctx).a)).method())!.b : undefined)');
expect(block).toContain('((null as any) ? ((ctx).a())!.method() : undefined)');
});
it('should not check the presence of a property/method on the receiver when disabled', () => {
const DISABLED_CONFIG:
TypeCheckingConfig = {...BASE_CONFIG, strictSafeNavigationTypes: false};
const block = tcb(TEMPLATE, DIRECTIVES, DISABLED_CONFIG);
expect(block).toContain('((((ctx).a).method()) as any).b');
expect(block).toContain('(((((ctx).a)).method()) as any).b');
expect(block).toContain('(((ctx).a()) as any).method()');
});
});

View File

@ -902,8 +902,7 @@ export declare class AnimationEvent {
expect(diags.length).toBe(1);
expect(diags[0].messageText)
.toEqual(`Property 'does_not_exist' does not exist on type '{ name: string; }'.`);
expect(diags[0].start).toBe(199);
expect(diags[0].length).toBe(19);
expect(getSourceCodeForDiagnostic(diags[0])).toBe('does_not_exist');
});
it('should accept an NgFor iteration over an any-typed value', () => {
@ -1126,8 +1125,7 @@ export declare class AnimationEvent {
const diags = env.driveDiagnostics();
expect(diags.length).toBe(1);
expect(diags[0].messageText).toEqual(`Property 'does_not_exist' does not exist on type 'T'.`);
expect(diags[0].start).toBe(206);
expect(diags[0].length).toBe(19);
expect(getSourceCodeForDiagnostic(diags[0])).toBe('does_not_exist');
});
describe('microsyntax variables', () => {
@ -1752,7 +1750,7 @@ export declare class AnimationEvent {
const diags = await driveDiagnostics();
expect(diags.length).toBe(1);
expect(diags[0].file!.fileName).toBe(_('/test.ts'));
expect(getSourceCodeForDiagnostic(diags[0])).toBe('user.does_not_exist');
expect(getSourceCodeForDiagnostic(diags[0])).toBe('does_not_exist');
});
it('should be correct for indirect templates', async () => {
@ -1774,7 +1772,7 @@ export declare class AnimationEvent {
const diags = await driveDiagnostics();
expect(diags.length).toBe(1);
expect(diags[0].file!.fileName).toBe(_('/test.ts') + ' (TestCmp template)');
expect(getSourceCodeForDiagnostic(diags[0])).toBe('user.does_not_exist');
expect(getSourceCodeForDiagnostic(diags[0])).toBe('does_not_exist');
expect(getSourceCodeForDiagnostic(diags[0].relatedInformation![0])).toBe('TEMPLATE');
});
@ -1797,7 +1795,7 @@ export declare class AnimationEvent {
const diags = await driveDiagnostics();
expect(diags.length).toBe(1);
expect(diags[0].file!.fileName).toBe(_('/template.html'));
expect(getSourceCodeForDiagnostic(diags[0])).toBe('user.does_not_exist');
expect(getSourceCodeForDiagnostic(diags[0])).toBe('does_not_exist');
expect(getSourceCodeForDiagnostic(diags[0].relatedInformation![0]))
.toBe(`'./template.html'`);
});