feat(ivy): check regular attributes that correspond with directive inputs (#33066)

Prior to this change, a static attribute that corresponds with a
directive's input would not be type-checked against the type of the
input. This is unfortunate, as a static value always has type `string`,
whereas the directive's input type might be something different. This
typically occurs when a developer forgets to enclose the attribute name
in brackets to make it a property binding.

This commit lets static attributes be considered as bindings with string
values, so that they will be properly type-checked.

PR Close #33066
This commit is contained in:
JoostK 2019-10-10 20:22:38 +02:00 committed by Miško Hevery
parent f05999730a
commit cd7b199219
4 changed files with 62 additions and 15 deletions

View File

@ -27,7 +27,7 @@ export interface TabInfo {
<div #content style="display: none"><ng-content></ng-content></div> <div #content style="display: none"><ng-content></ng-content></div>
<mat-card> <mat-card>
<mat-tab-group class="code-tab-group" disableRipple> <mat-tab-group class="code-tab-group" [disableRipple]="true">
<mat-tab style="overflow-y: hidden;" *ngFor="let tab of tabs"> <mat-tab style="overflow-y: hidden;" *ngFor="let tab of tabs">
<ng-template mat-tab-label> <ng-template mat-tab-label>
<span class="{{ tab.class }}">{{ tab.header }}</span> <span class="{{ tab.class }}">{{ tab.header }}</span>

View File

@ -840,6 +840,7 @@ function tcbGetDirectiveInputs(
const unsetFields = new Set(propMatch.values()); const unsetFields = new Set(propMatch.values());
el.inputs.forEach(processAttribute); el.inputs.forEach(processAttribute);
el.attributes.forEach(processAttribute);
if (el instanceof TmplAstTemplate) { if (el instanceof TmplAstTemplate) {
el.templateAttrs.forEach(processAttribute); el.templateAttrs.forEach(processAttribute);
} }
@ -856,14 +857,24 @@ function tcbGetDirectiveInputs(
* a matching binding. * a matching binding.
*/ */
function processAttribute(attr: TmplAstBoundAttribute | TmplAstTextAttribute): void { function processAttribute(attr: TmplAstBoundAttribute | TmplAstTextAttribute): void {
if (attr instanceof TmplAstBoundAttribute && propMatch.has(attr.name)) { // Skip the attribute if the directive does not have an input for it.
if (!propMatch.has(attr.name)) {
return;
}
const field = propMatch.get(attr.name) !; const field = propMatch.get(attr.name) !;
// Remove the field from the set of unseen fields, now that it's been assigned to. // Remove the field from the set of unseen fields, now that it's been assigned to.
unsetFields.delete(field); unsetFields.delete(field);
let expr: ts.Expression;
if (attr instanceof TmplAstBoundAttribute) {
// Produce an expression representing the value of the binding. // Produce an expression representing the value of the binding.
const expr = tcbExpression(attr.value, tcb, scope, attr.valueSpan || attr.sourceSpan); expr = tcbExpression(attr.value, tcb, scope, attr.valueSpan || attr.sourceSpan);
} else {
// For regular attributes with a static string value, use the represented string literal.
expr = ts.createStringLiteral(attr.value);
}
directiveInputs.push({ directiveInputs.push({
type: 'binding', type: 'binding',
field: field, field: field,
@ -871,7 +882,6 @@ function tcbGetDirectiveInputs(
sourceSpan: attr.sourceSpan, sourceSpan: attr.sourceSpan,
}); });
} }
}
} }
/** /**

View File

@ -35,6 +35,17 @@ describe('type check blocks', () => {
expect(tcb(TEMPLATE)).toContain('((ctx).a)[(ctx).b];'); expect(tcb(TEMPLATE)).toContain('((ctx).a)[(ctx).b];');
}); });
it('should handle attribute values for directive inputs', () => {
const TEMPLATE = `<div dir inputA="value"></div>`;
const DIRECTIVES: TestDeclaration[] = [{
type: 'directive',
name: 'DirA',
selector: '[dir]',
inputs: {inputA: 'inputA'},
}];
expect(tcb(TEMPLATE, DIRECTIVES)).toContain('inputA: ("value")');
});
it('should handle empty bindings', () => { it('should handle empty bindings', () => {
const TEMPLATE = `<div dir-a [inputA]=""></div>`; const TEMPLATE = `<div dir-a [inputA]=""></div>`;
const DIRECTIVES: TestDeclaration[] = [{ const DIRECTIVES: TestDeclaration[] = [{

View File

@ -80,6 +80,32 @@ export declare class CommonModule {
env.driveMain(); env.driveMain();
}); });
it('should check regular attributes that are directive inputs', () => {
env.write('test.ts', `
import {Component, Directive, NgModule, Input} from '@angular/core';
@Component({
selector: 'test',
template: '<div dir foo="2"></div>',
})
class TestCmp {}
@Directive({selector: '[dir]'})
class TestDir {
@Input() foo: number;
}
@NgModule({
declarations: [TestCmp, TestDir],
})
class Module {}
`);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(1);
expect(diags[0].messageText).toEqual(`Type 'string' is not assignable to type 'number'.`);
});
it('should check basic usage of NgIf', () => { it('should check basic usage of NgIf', () => {
env.write('test.ts', ` env.write('test.ts', `
import {CommonModule} from '@angular/common'; import {CommonModule} from '@angular/common';