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:
parent
f05999730a
commit
cd7b199219
|
@ -27,7 +27,7 @@ export interface TabInfo {
|
|||
<div #content style="display: none"><ng-content></ng-content></div>
|
||||
|
||||
<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">
|
||||
<ng-template mat-tab-label>
|
||||
<span class="{{ tab.class }}">{{ tab.header }}</span>
|
||||
|
|
|
@ -840,6 +840,7 @@ function tcbGetDirectiveInputs(
|
|||
const unsetFields = new Set(propMatch.values());
|
||||
|
||||
el.inputs.forEach(processAttribute);
|
||||
el.attributes.forEach(processAttribute);
|
||||
if (el instanceof TmplAstTemplate) {
|
||||
el.templateAttrs.forEach(processAttribute);
|
||||
}
|
||||
|
@ -856,21 +857,30 @@ function tcbGetDirectiveInputs(
|
|||
* a matching binding.
|
||||
*/
|
||||
function processAttribute(attr: TmplAstBoundAttribute | TmplAstTextAttribute): void {
|
||||
if (attr instanceof TmplAstBoundAttribute && propMatch.has(attr.name)) {
|
||||
const field = propMatch.get(attr.name) !;
|
||||
|
||||
// Remove the field from the set of unseen fields, now that it's been assigned to.
|
||||
unsetFields.delete(field);
|
||||
|
||||
// Produce an expression representing the value of the binding.
|
||||
const expr = tcbExpression(attr.value, tcb, scope, attr.valueSpan || attr.sourceSpan);
|
||||
directiveInputs.push({
|
||||
type: 'binding',
|
||||
field: field,
|
||||
expression: expr,
|
||||
sourceSpan: attr.sourceSpan,
|
||||
});
|
||||
// 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) !;
|
||||
|
||||
// Remove the field from the set of unseen fields, now that it's been assigned to.
|
||||
unsetFields.delete(field);
|
||||
|
||||
let expr: ts.Expression;
|
||||
if (attr instanceof TmplAstBoundAttribute) {
|
||||
// Produce an expression representing the value of the binding.
|
||||
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({
|
||||
type: 'binding',
|
||||
field: field,
|
||||
expression: expr,
|
||||
sourceSpan: attr.sourceSpan,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -35,6 +35,17 @@ describe('type check blocks', () => {
|
|||
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', () => {
|
||||
const TEMPLATE = `<div dir-a [inputA]=""></div>`;
|
||||
const DIRECTIVES: TestDeclaration[] = [{
|
||||
|
|
|
@ -80,6 +80,32 @@ export declare class CommonModule {
|
|||
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', () => {
|
||||
env.write('test.ts', `
|
||||
import {CommonModule} from '@angular/common';
|
||||
|
|
Loading…
Reference in New Issue