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>
|
<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>
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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[] = [{
|
||||||
|
|
|
@ -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';
|
||||||
|
|
Loading…
Reference in New Issue