feat(compiler-cli): Add compiler option to report errors when assigning to restricted input fields (#38249)

The compiler does not currently report errors when there's an `@Input()`
for a `private`, `protected`, or `readonly` directive/component class member.
This change adds an option to enable reporting errors when a template
attempts to bind to one of these restricted input fields.

PR Close #38249
This commit is contained in:
Andrew Scott 2020-07-28 14:51:14 -07:00 committed by Andrew Kushnir
parent fa0104017a
commit 71138f6004
13 changed files with 236 additions and 59 deletions

View File

@ -114,6 +114,7 @@ In case of a false positive like these, there are a few options:
|Strictness flag|Effect| |Strictness flag|Effect|
|-|-| |-|-|
|`strictInputTypes`|Whether the assignability of a binding expression to the `@Input()` field is checked. Also affects the inference of directive generic types. | |`strictInputTypes`|Whether the assignability of a binding expression to the `@Input()` field is checked. Also affects the inference of directive generic types. |
|`strictInputAccessModifiers`|Whether access modifiers such as `private`/`protected`/`readonly` are honored when assigning a binding expression to an `@Input()`. If disabled, the access modifiers of the `@Input` are ignored; only the type is checked.|
|`strictNullInputTypes`|Whether `strictNullChecks` is honored when checking `@Input()` bindings (per `strictInputTypes`). Turning this off can be useful when using a library that was not built with `strictNullChecks` in mind.| |`strictNullInputTypes`|Whether `strictNullChecks` is honored when checking `@Input()` bindings (per `strictInputTypes`). Turning this off can be useful when using a library that was not built with `strictNullChecks` in mind.|
|`strictAttributeTypes`|Whether to check `@Input()` bindings that are made using text attributes (for example, `<mat-tab label="Step 1">` vs `<mat-tab [label]="'Step 1'">`). |`strictAttributeTypes`|Whether to check `@Input()` bindings that are made using text attributes (for example, `<mat-tab label="Step 1">` vs `<mat-tab [label]="'Step 1'">`).
|`strictSafeNavigationTypes`|Whether the return type of safe navigation operations (for example, `user?.name`) will be correctly inferred based on the type of `user`). If disabled, `user?.name` will be of type `any`. |`strictSafeNavigationTypes`|Whether the return type of safe navigation operations (for example, `user?.name`) will be correctly inferred based on the type of `user`). If disabled, `user?.name` will be of type `any`.

View File

@ -35,6 +35,7 @@ export interface StrictTemplateOptions {
strictContextGenerics?: boolean; strictContextGenerics?: boolean;
strictDomEventTypes?: boolean; strictDomEventTypes?: boolean;
strictDomLocalRefTypes?: boolean; strictDomLocalRefTypes?: boolean;
strictInputAccessModifiers?: boolean;
strictInputTypes?: boolean; strictInputTypes?: boolean;
strictLiteralTypes?: boolean; strictLiteralTypes?: boolean;
strictNullInputTypes?: boolean; strictNullInputTypes?: boolean;

View File

@ -147,6 +147,18 @@ export interface StrictTemplateOptions {
*/ */
strictInputTypes?: boolean; strictInputTypes?: boolean;
/**
* Whether to check if the input binding attempts to assign to a restricted field (readonly,
* private, or protected) on the directive/component.
*
* Defaults to `false`, even if "fullTemplateTypeCheck", "strictTemplates" and/or
* "strictInputTypes" is set. Note that if `strictInputTypes` is not set, or set to `false`, this
* flag has no effect.
*
* Tracking issue for enabling this by default: https://github.com/angular/angular/issues/38400
*/
strictInputAccessModifiers?: boolean;
/** /**
* Whether to use strict null types for input bindings for directives. * Whether to use strict null types for input bindings for directives.
* *

View File

@ -415,6 +415,7 @@ export class NgCompiler {
checkQueries: false, checkQueries: false,
checkTemplateBodies: true, checkTemplateBodies: true,
checkTypeOfInputBindings: strictTemplates, checkTypeOfInputBindings: strictTemplates,
honorAccessModifiersForInputBindings: false,
strictNullInputBindings: strictTemplates, strictNullInputBindings: strictTemplates,
checkTypeOfAttributes: strictTemplates, checkTypeOfAttributes: strictTemplates,
// Even in full template type-checking mode, DOM binding checks are not quite ready yet. // Even in full template type-checking mode, DOM binding checks are not quite ready yet.
@ -442,6 +443,7 @@ export class NgCompiler {
checkTemplateBodies: false, checkTemplateBodies: false,
checkTypeOfInputBindings: false, checkTypeOfInputBindings: false,
strictNullInputBindings: false, strictNullInputBindings: false,
honorAccessModifiersForInputBindings: false,
checkTypeOfAttributes: false, checkTypeOfAttributes: false,
checkTypeOfDomBindings: false, checkTypeOfDomBindings: false,
checkTypeOfOutputEvents: false, checkTypeOfOutputEvents: false,
@ -462,6 +464,10 @@ export class NgCompiler {
typeCheckingConfig.checkTypeOfInputBindings = this.options.strictInputTypes; typeCheckingConfig.checkTypeOfInputBindings = this.options.strictInputTypes;
typeCheckingConfig.applyTemplateContextGuards = this.options.strictInputTypes; typeCheckingConfig.applyTemplateContextGuards = this.options.strictInputTypes;
} }
if (this.options.strictInputAccessModifiers !== undefined) {
typeCheckingConfig.honorAccessModifiersForInputBindings =
this.options.strictInputAccessModifiers;
}
if (this.options.strictNullInputTypes !== undefined) { if (this.options.strictNullInputTypes !== undefined) {
typeCheckingConfig.strictNullInputBindings = this.options.strictNullInputTypes; typeCheckingConfig.strictNullInputBindings = this.options.strictNullInputTypes;
} }

View File

@ -60,6 +60,13 @@ export interface DirectiveTypeCheckMeta {
*/ */
restrictedInputFields: Set<string>; restrictedInputFields: Set<string>;
/**
* The set of input fields which are declared as string literal members in the Directive's class.
* We need to track these separately because these fields may not be valid JS identifiers so
* we cannot use them with property access expressions when assigning inputs.
*/
stringLiteralInputFields: Set<string>;
/** /**
* The set of input fields which do not have corresponding members in the Directive's class. * The set of input fields which do not have corresponding members in the Directive's class.
*/ */

View File

@ -27,9 +27,10 @@ export function flattenInheritedDirectiveMetadata(
let inputs: {[key: string]: string|[string, string]} = {}; let inputs: {[key: string]: string|[string, string]} = {};
let outputs: {[key: string]: string} = {}; let outputs: {[key: string]: string} = {};
let coercedInputFields = new Set<string>(); const coercedInputFields = new Set<string>();
let undeclaredInputFields = new Set<string>(); const undeclaredInputFields = new Set<string>();
let restrictedInputFields = new Set<string>(); const restrictedInputFields = new Set<string>();
const stringLiteralInputFields = new Set<string>();
let isDynamic = false; let isDynamic = false;
const addMetadata = (meta: DirectiveMeta): void => { const addMetadata = (meta: DirectiveMeta): void => {
@ -56,6 +57,9 @@ export function flattenInheritedDirectiveMetadata(
for (const restrictedInputField of meta.restrictedInputFields) { for (const restrictedInputField of meta.restrictedInputFields) {
restrictedInputFields.add(restrictedInputField); restrictedInputFields.add(restrictedInputField);
} }
for (const field of meta.stringLiteralInputFields) {
stringLiteralInputFields.add(field);
}
}; };
addMetadata(topMeta); addMetadata(topMeta);
@ -67,6 +71,7 @@ export function flattenInheritedDirectiveMetadata(
coercedInputFields, coercedInputFields,
undeclaredInputFields, undeclaredInputFields,
restrictedInputFields, restrictedInputFields,
stringLiteralInputFields,
baseClass: isDynamic ? 'dynamic' : null, baseClass: isDynamic ? 'dynamic' : null,
}; };
} }

View File

@ -98,15 +98,21 @@ export function extractDirectiveTypeCheckMeta(
.filter((inputName): inputName is string => inputName !== null)); .filter((inputName): inputName is string => inputName !== null));
const restrictedInputFields = new Set<string>(); const restrictedInputFields = new Set<string>();
const stringLiteralInputFields = new Set<string>();
const undeclaredInputFields = new Set<string>(); const undeclaredInputFields = new Set<string>();
for (const fieldName of Object.keys(inputs)) { for (const fieldName of Object.keys(inputs)) {
const field = members.find(member => member.name === fieldName); const field = members.find(member => member.name === fieldName);
if (field === undefined || field.node === null) { if (field === undefined || field.node === null) {
undeclaredInputFields.add(fieldName); undeclaredInputFields.add(fieldName);
} else if (isRestricted(field.node)) { continue;
}
if (isRestricted(field.node)) {
restrictedInputFields.add(fieldName); restrictedInputFields.add(fieldName);
} }
if (field.nameNode !== null && ts.isStringLiteral(field.nameNode)) {
stringLiteralInputFields.add(fieldName);
}
} }
const arity = reflector.getGenericArityOfClass(node); const arity = reflector.getGenericArityOfClass(node);
@ -116,6 +122,7 @@ export function extractDirectiveTypeCheckMeta(
ngTemplateGuards, ngTemplateGuards,
coercedInputFields, coercedInputFields,
restrictedInputFields, restrictedInputFields,
stringLiteralInputFields,
undeclaredInputFields, undeclaredInputFields,
isGeneric: arity !== null && arity > 0, isGeneric: arity !== null && arity > 0,
}; };

View File

@ -244,6 +244,7 @@ function fakeDirective(ref: Reference<ClassDeclaration>): DirectiveMeta {
ngTemplateGuards: [], ngTemplateGuards: [],
coercedInputFields: new Set<string>(), coercedInputFields: new Set<string>(),
restrictedInputFields: new Set<string>(), restrictedInputFields: new Set<string>(),
stringLiteralInputFields: new Set<string>(),
undeclaredInputFields: new Set<string>(), undeclaredInputFields: new Set<string>(),
isGeneric: false, isGeneric: false,
baseClass: null, baseClass: null,

View File

@ -90,6 +90,14 @@ export interface TypeCheckingConfig {
*/ */
checkTypeOfInputBindings: boolean; checkTypeOfInputBindings: boolean;
/**
* Whether to honor the access modifiers on input bindings for the component/directive.
*
* If a template binding attempts to assign to an input that is private/protected/readonly,
* this will produce errors when enabled but will not when disabled.
*/
honorAccessModifiersForInputBindings: boolean;
/** /**
* Whether to use strict null types for input bindings for directives. * Whether to use strict null types for input bindings for directives.
* *

View File

@ -453,8 +453,13 @@ class TcbDirectiveInputsOp extends TcbOp {
// declared in a `@Directive` or `@Component` decorator's `inputs` property) there is no // declared in a `@Directive` or `@Component` decorator's `inputs` property) there is no
// assignment target available, so this field is skipped. // assignment target available, so this field is skipped.
continue; continue;
} else if (this.dir.restrictedInputFields.has(fieldName)) { } else if (
// To ignore errors, assign to temp variable with type of the field !this.tcb.env.config.honorAccessModifiersForInputBindings &&
this.dir.restrictedInputFields.has(fieldName)) {
// If strict checking of access modifiers is disabled and the field is restricted
// (i.e. private/protected/readonly), generate an assignment into a temporary variable
// that has the type of the field. This achieves type-checking but circumvents the access
// modifiers.
const id = this.tcb.allocateId(); const id = this.tcb.allocateId();
const dirTypeRef = this.tcb.env.referenceType(this.dir.ref); const dirTypeRef = this.tcb.env.referenceType(this.dir.ref);
if (!ts.isTypeReferenceNode(dirTypeRef)) { if (!ts.isTypeReferenceNode(dirTypeRef)) {
@ -464,23 +469,16 @@ class TcbDirectiveInputsOp extends TcbOp {
const type = ts.createIndexedAccessTypeNode( const type = ts.createIndexedAccessTypeNode(
ts.createTypeQueryNode(dirId as ts.Identifier), ts.createTypeQueryNode(dirId as ts.Identifier),
ts.createLiteralTypeNode(ts.createStringLiteral(fieldName))); ts.createLiteralTypeNode(ts.createStringLiteral(fieldName)));
const temp = tsCreateVariable(id, ts.createNonNullExpression(ts.createNull()), type); const temp = tsDeclareVariable(id, type);
addParseSpanInfo(temp, input.attribute.sourceSpan);
this.scope.addStatement(temp); this.scope.addStatement(temp);
target = id; target = id;
// TODO: To get errors assign directly to the fields on the instance, using dot access
// when possible
} else { } else {
// Otherwise, a declaration exists in which case the `dir["fieldName"]` syntax is used // To get errors assign directly to the fields on the instance, using property access
// as assignment target. An element access is used instead of a property access to // when possible. String literal fields may not be valid JS identifiers so we use
// support input names that are not valid JavaScript identifiers. Additionally, using // literal element access instead for those cases.
// element access syntax does not produce target = this.dir.stringLiteralInputFields.has(fieldName) ?
// TS2341 "Property $prop is private and only accessible within class $class." nor ts.createElementAccess(dirId, ts.createStringLiteral(fieldName)) :
// TS2445 "Property $prop is protected and only accessible within class $class and its ts.createPropertyAccess(dirId, ts.createIdentifier(fieldName));
// subclasses."
target = ts.createElementAccess(dirId, ts.createStringLiteral(fieldName));
} }
// Finally the assignment is extended by assigning it into the target expression. // Finally the assignment is extended by assigning it into the target expression.

View File

@ -157,6 +157,7 @@ export const ALL_ENABLED_CONFIG: TypeCheckingConfig = {
checkQueries: false, checkQueries: false,
checkTemplateBodies: true, checkTemplateBodies: true,
checkTypeOfInputBindings: true, checkTypeOfInputBindings: true,
honorAccessModifiersForInputBindings: true,
strictNullInputBindings: true, strictNullInputBindings: true,
checkTypeOfAttributes: true, checkTypeOfAttributes: true,
// Feature is still in development. // Feature is still in development.
@ -178,10 +179,11 @@ export type TestDirective = Partial<Pick<
TypeCheckableDirectiveMeta, TypeCheckableDirectiveMeta,
Exclude< Exclude<
keyof TypeCheckableDirectiveMeta, keyof TypeCheckableDirectiveMeta,
'ref'|'coercedInputFields'|'restrictedInputFields'|'undeclaredInputFields'>>>&{ 'ref'|'coercedInputFields'|'restrictedInputFields'|'stringLiteralInputFields'|
'undeclaredInputFields'>>>&{
selector: string, name: string, file?: AbsoluteFsPath, type: 'directive', selector: string, name: string, file?: AbsoluteFsPath, type: 'directive',
coercedInputFields?: string[], restrictedInputFields?: string[], coercedInputFields?: string[], restrictedInputFields?: string[],
undeclaredInputFields?: string[], isGeneric?: boolean; stringLiteralInputFields?: string[], undeclaredInputFields?: string[], isGeneric?: boolean;
}; };
export type TestPipe = { export type TestPipe = {
name: string, name: string,
@ -212,6 +214,7 @@ export function tcb(
applyTemplateContextGuards: true, applyTemplateContextGuards: true,
checkQueries: false, checkQueries: false,
checkTypeOfInputBindings: true, checkTypeOfInputBindings: true,
honorAccessModifiersForInputBindings: false,
strictNullInputBindings: true, strictNullInputBindings: true,
checkTypeOfAttributes: true, checkTypeOfAttributes: true,
checkTypeOfDomBindings: false, checkTypeOfDomBindings: false,
@ -420,6 +423,7 @@ function prepareDeclarations(
ngTemplateGuards: decl.ngTemplateGuards || [], ngTemplateGuards: decl.ngTemplateGuards || [],
coercedInputFields: new Set<string>(decl.coercedInputFields || []), coercedInputFields: new Set<string>(decl.coercedInputFields || []),
restrictedInputFields: new Set<string>(decl.restrictedInputFields || []), restrictedInputFields: new Set<string>(decl.restrictedInputFields || []),
stringLiteralInputFields: new Set<string>(decl.stringLiteralInputFields || []),
undeclaredInputFields: new Set<string>(decl.undeclaredInputFields || []), undeclaredInputFields: new Set<string>(decl.undeclaredInputFields || []),
isGeneric: decl.isGeneric ?? false, isGeneric: decl.isGeneric ?? false,
outputs: decl.outputs || {}, outputs: decl.outputs || {},

View File

@ -55,7 +55,7 @@ describe('type check blocks', () => {
selector: '[dir]', selector: '[dir]',
inputs: {inputA: 'inputA'}, inputs: {inputA: 'inputA'},
}]; }];
expect(tcb(TEMPLATE, DIRECTIVES)).toContain('_t2: DirA = (null!); _t2["inputA"] = ("value");'); expect(tcb(TEMPLATE, DIRECTIVES)).toContain('_t2: DirA = (null!); _t2.inputA = ("value");');
}); });
it('should handle multiple bindings to the same property', () => { it('should handle multiple bindings to the same property', () => {
@ -67,8 +67,8 @@ describe('type check blocks', () => {
inputs: {inputA: 'inputA'}, inputs: {inputA: 'inputA'},
}]; }];
const block = tcb(TEMPLATE, DIRECTIVES); const block = tcb(TEMPLATE, DIRECTIVES);
expect(block).toContain('_t2["inputA"] = (1);'); expect(block).toContain('_t2.inputA = (1);');
expect(block).toContain('_t2["inputA"] = (2);'); expect(block).toContain('_t2.inputA = (2);');
}); });
it('should handle empty bindings', () => { it('should handle empty bindings', () => {
@ -79,7 +79,7 @@ describe('type check blocks', () => {
selector: '[dir-a]', selector: '[dir-a]',
inputs: {inputA: 'inputA'}, inputs: {inputA: 'inputA'},
}]; }];
expect(tcb(TEMPLATE, DIRECTIVES)).toContain('_t2["inputA"] = (undefined);'); expect(tcb(TEMPLATE, DIRECTIVES)).toContain('_t2.inputA = (undefined);');
}); });
it('should handle bindings without value', () => { it('should handle bindings without value', () => {
@ -90,7 +90,7 @@ describe('type check blocks', () => {
selector: '[dir-a]', selector: '[dir-a]',
inputs: {inputA: 'inputA'}, inputs: {inputA: 'inputA'},
}]; }];
expect(tcb(TEMPLATE, DIRECTIVES)).toContain('_t2["inputA"] = (undefined);'); expect(tcb(TEMPLATE, DIRECTIVES)).toContain('_t2.inputA = (undefined);');
}); });
it('should handle implicit vars on ng-template', () => { it('should handle implicit vars on ng-template', () => {
@ -322,7 +322,7 @@ describe('type check blocks', () => {
expect(tcb(TEMPLATE, DIRECTIVES)) expect(tcb(TEMPLATE, DIRECTIVES))
.toContain( .toContain(
'var _t2: Dir = (null!); ' + 'var _t2: Dir = (null!); ' +
'_t2["input"] = (_t2);'); '_t2.input = (_t2);');
}); });
it('should generate circular references between two directives correctly', () => { it('should generate circular references between two directives correctly', () => {
@ -350,9 +350,9 @@ describe('type check blocks', () => {
.toContain( .toContain(
'var _t2: DirA = (null!); ' + 'var _t2: DirA = (null!); ' +
'var _t3: DirB = (null!); ' + 'var _t3: DirB = (null!); ' +
'_t2["inputA"] = (_t3); ' + '_t2.inputA = (_t3); ' +
'var _t4 = document.createElement("div"); ' + 'var _t4 = document.createElement("div"); ' +
'_t3["inputA"] = (_t2);'); '_t3.inputA = (_t2);');
}); });
it('should handle undeclared properties', () => { it('should handle undeclared properties', () => {
@ -372,7 +372,7 @@ describe('type check blocks', () => {
'(((ctx).foo)); '); '(((ctx).foo)); ');
}); });
it('should handle restricted properties', () => { it('should assign restricted properties to temp variables by default', () => {
const TEMPLATE = `<div dir [inputA]="foo"></div>`; const TEMPLATE = `<div dir [inputA]="foo"></div>`;
const DIRECTIVES: TestDeclaration[] = [{ const DIRECTIVES: TestDeclaration[] = [{
type: 'directive', type: 'directive',
@ -390,6 +390,24 @@ describe('type check blocks', () => {
'_t3 = (((ctx).foo)); '); '_t3 = (((ctx).foo)); ');
}); });
it('should assign properties via element access for field names that are not JS identifiers',
() => {
const TEMPLATE = `<div dir [inputA]="foo"></div>`;
const DIRECTIVES: TestDeclaration[] = [{
type: 'directive',
name: 'Dir',
selector: '[dir]',
inputs: {
'some-input.xs': 'inputA',
},
stringLiteralInputFields: ['some-input.xs'],
}];
const block = tcb(TEMPLATE, DIRECTIVES);
expect(block).toContain(
'var _t2: Dir = (null!); ' +
'_t2["some-input.xs"] = (((ctx).foo)); ');
});
it('should handle a single property bound to multiple fields', () => { it('should handle a single property bound to multiple fields', () => {
const TEMPLATE = `<div dir [inputA]="foo"></div>`; const TEMPLATE = `<div dir [inputA]="foo"></div>`;
const DIRECTIVES: TestDeclaration[] = [{ const DIRECTIVES: TestDeclaration[] = [{
@ -404,7 +422,7 @@ describe('type check blocks', () => {
expect(tcb(TEMPLATE, DIRECTIVES)) expect(tcb(TEMPLATE, DIRECTIVES))
.toContain( .toContain(
'var _t2: Dir = (null!); ' + 'var _t2: Dir = (null!); ' +
'_t2["field2"] = _t2["field1"] = (((ctx).foo));'); '_t2.field2 = _t2.field1 = (((ctx).foo));');
}); });
it('should handle a single property bound to multiple fields, where one of them is coerced', it('should handle a single property bound to multiple fields, where one of them is coerced',
@ -424,7 +442,7 @@ describe('type check blocks', () => {
.toContain( .toContain(
'var _t2: Dir = (null!); ' + 'var _t2: Dir = (null!); ' +
'var _t3: typeof Dir.ngAcceptInputType_field1 = (null!); ' + 'var _t3: typeof Dir.ngAcceptInputType_field1 = (null!); ' +
'_t2["field2"] = _t3 = (((ctx).foo));'); '_t2.field2 = _t3 = (((ctx).foo));');
}); });
it('should handle a single property bound to multiple fields, where one of them is undeclared', it('should handle a single property bound to multiple fields, where one of them is undeclared',
@ -443,7 +461,7 @@ describe('type check blocks', () => {
expect(tcb(TEMPLATE, DIRECTIVES)) expect(tcb(TEMPLATE, DIRECTIVES))
.toContain( .toContain(
'var _t2: Dir = (null!); ' + 'var _t2: Dir = (null!); ' +
'_t2["field2"] = (((ctx).foo));'); '_t2.field2 = (((ctx).foo));');
}); });
it('should use coercion types if declared', () => { it('should use coercion types if declared', () => {
@ -590,6 +608,7 @@ describe('type check blocks', () => {
checkQueries: false, checkQueries: false,
checkTemplateBodies: true, checkTemplateBodies: true,
checkTypeOfInputBindings: true, checkTypeOfInputBindings: true,
honorAccessModifiersForInputBindings: false,
strictNullInputBindings: true, strictNullInputBindings: true,
checkTypeOfAttributes: true, checkTypeOfAttributes: true,
checkTypeOfDomBindings: false, checkTypeOfDomBindings: false,
@ -639,14 +658,14 @@ describe('type check blocks', () => {
it('should include null and undefined when enabled', () => { it('should include null and undefined when enabled', () => {
const block = tcb(TEMPLATE, DIRECTIVES); const block = tcb(TEMPLATE, DIRECTIVES);
expect(block).toContain('_t2["dirInput"] = (((ctx).a));'); expect(block).toContain('_t2.dirInput = (((ctx).a));');
expect(block).toContain('((ctx).b);'); expect(block).toContain('((ctx).b);');
}); });
it('should use the non-null assertion operator when disabled', () => { it('should use the non-null assertion operator when disabled', () => {
const DISABLED_CONFIG: const DISABLED_CONFIG:
TypeCheckingConfig = {...BASE_CONFIG, strictNullInputBindings: false}; TypeCheckingConfig = {...BASE_CONFIG, strictNullInputBindings: false};
const block = tcb(TEMPLATE, DIRECTIVES, DISABLED_CONFIG); const block = tcb(TEMPLATE, DIRECTIVES, DISABLED_CONFIG);
expect(block).toContain('_t2["dirInput"] = (((ctx).a)!);'); expect(block).toContain('_t2.dirInput = (((ctx).a)!);');
expect(block).toContain('((ctx).b)!;'); expect(block).toContain('((ctx).b)!;');
}); });
}); });
@ -655,7 +674,7 @@ describe('type check blocks', () => {
it('should check types of bindings when enabled', () => { it('should check types of bindings when enabled', () => {
const TEMPLATE = `<div dir [dirInput]="a" [nonDirInput]="b"></div>`; const TEMPLATE = `<div dir [dirInput]="a" [nonDirInput]="b"></div>`;
const block = tcb(TEMPLATE, DIRECTIVES); const block = tcb(TEMPLATE, DIRECTIVES);
expect(block).toContain('_t2["dirInput"] = (((ctx).a));'); expect(block).toContain('_t2.dirInput = (((ctx).a));');
expect(block).toContain('((ctx).b);'); expect(block).toContain('((ctx).b);');
}); });
@ -664,7 +683,7 @@ describe('type check blocks', () => {
const DISABLED_CONFIG: const DISABLED_CONFIG:
TypeCheckingConfig = {...BASE_CONFIG, checkTypeOfInputBindings: false}; TypeCheckingConfig = {...BASE_CONFIG, checkTypeOfInputBindings: false};
const block = tcb(TEMPLATE, DIRECTIVES, DISABLED_CONFIG); const block = tcb(TEMPLATE, DIRECTIVES, DISABLED_CONFIG);
expect(block).toContain('_t2["dirInput"] = ((((ctx).a) as any));'); expect(block).toContain('_t2.dirInput = ((((ctx).a) as any));');
expect(block).toContain('(((ctx).b) as any);'); expect(block).toContain('(((ctx).b) as any);');
}); });
@ -673,7 +692,7 @@ describe('type check blocks', () => {
const DISABLED_CONFIG: const DISABLED_CONFIG:
TypeCheckingConfig = {...BASE_CONFIG, checkTypeOfInputBindings: false}; TypeCheckingConfig = {...BASE_CONFIG, checkTypeOfInputBindings: false};
const block = tcb(TEMPLATE, DIRECTIVES, DISABLED_CONFIG); const block = tcb(TEMPLATE, DIRECTIVES, DISABLED_CONFIG);
expect(block).toContain('_t2["dirInput"] = ((((((ctx).a)) === (((ctx).b))) as any));'); expect(block).toContain('_t2.dirInput = ((((((ctx).a)) === (((ctx).b))) as any));');
}); });
}); });
@ -793,9 +812,9 @@ describe('type check blocks', () => {
it('should assign string value to the input when enabled', () => { it('should assign string value to the input when enabled', () => {
const block = tcb(TEMPLATE, DIRECTIVES); const block = tcb(TEMPLATE, DIRECTIVES);
expect(block).toContain('_t2["disabled"] = ("");'); expect(block).toContain('_t2.disabled = ("");');
expect(block).toContain('_t2["cols"] = ("3");'); expect(block).toContain('_t2.cols = ("3");');
expect(block).toContain('_t2["rows"] = (2);'); expect(block).toContain('_t2.rows = (2);');
}); });
it('should use any for attributes but still check bound attributes when disabled', () => { it('should use any for attributes but still check bound attributes when disabled', () => {
@ -803,7 +822,7 @@ describe('type check blocks', () => {
const block = tcb(TEMPLATE, DIRECTIVES, DISABLED_CONFIG); const block = tcb(TEMPLATE, DIRECTIVES, DISABLED_CONFIG);
expect(block).not.toContain('"disabled"'); expect(block).not.toContain('"disabled"');
expect(block).not.toContain('"cols"'); expect(block).not.toContain('"cols"');
expect(block).toContain('_t2["rows"] = (2);'); expect(block).toContain('_t2.rows = (2);');
}); });
}); });
@ -873,5 +892,47 @@ describe('type check blocks', () => {
expect(block).toContain('function Test_TCB(ctx: Test<any>)'); expect(block).toContain('function Test_TCB(ctx: Test<any>)');
}); });
}); });
describe('config.checkAccessModifiersForInputBindings', () => {
const TEMPLATE = `<div dir [inputA]="foo"></div>`;
it('should assign restricted properties via element access for field names that are not JS identifiers',
() => {
const DIRECTIVES: TestDeclaration[] = [{
type: 'directive',
name: 'Dir',
selector: '[dir]',
inputs: {
'some-input.xs': 'inputA',
},
restrictedInputFields: ['some-input.xs'],
stringLiteralInputFields: ['some-input.xs'],
}];
const enableChecks:
TypeCheckingConfig = {...BASE_CONFIG, honorAccessModifiersForInputBindings: true};
const block = tcb(TEMPLATE, DIRECTIVES, enableChecks);
expect(block).toContain(
'var _t2: Dir = (null!); ' +
'_t2["some-input.xs"] = (((ctx).foo)); ');
});
it('should assign restricted properties via property access', () => {
const DIRECTIVES: TestDeclaration[] = [{
type: 'directive',
name: 'Dir',
selector: '[dir]',
inputs: {
fieldA: 'inputA',
},
restrictedInputFields: ['fieldA']
}];
const enableChecks:
TypeCheckingConfig = {...BASE_CONFIG, honorAccessModifiersForInputBindings: true};
const block = tcb(TEMPLATE, DIRECTIVES, enableChecks);
expect(block).toContain(
'var _t2: Dir = (null!); ' +
'_t2.fieldA = (((ctx).foo)); ');
});
});
}); });
}); });

View File

@ -1577,14 +1577,7 @@ export declare class AnimationEvent {
} }
`; `;
describe('with strict inputs', () => { const correctTypeInputsToRestrictedFields = `
beforeEach(() => {
env.tsconfig({fullTemplateTypeCheck: true, strictInputTypes: true});
});
it('should not produce diagnostics for correct inputs which assign to readonly, private, or protected fields',
() => {
env.write('test.ts', `
import {Component, NgModule, Input, Directive} from '@angular/core'; import {Component, NgModule, Input, Directive} from '@angular/core';
@Component({ @Component({
@ -1601,14 +1594,9 @@ export declare class AnimationEvent {
declarations: [FooCmp, TestDir], declarations: [FooCmp, TestDir],
}) })
export class FooModule {} export class FooModule {}
`); `;
const diags = env.driveDiagnostics();
expect(diags.length).toBe(0);
});
it('should not produce diagnostics for correct inputs which assign to readonly, private, or protected fields inherited from a base class', const correctInputsToRestrictedFieldsFromBaseClass = `
() => {
env.write('test.ts', `
import {Component, NgModule, Input, Directive} from '@angular/core'; import {Component, NgModule, Input, Directive} from '@angular/core';
@Component({ @Component({
@ -1629,7 +1617,85 @@ export declare class AnimationEvent {
declarations: [FooCmp, ChildDir], declarations: [FooCmp, ChildDir],
}) })
export class FooModule {} export class FooModule {}
`;
describe('with strictInputAccessModifiers', () => {
beforeEach(() => {
env.tsconfig({
fullTemplateTypeCheck: true,
strictInputTypes: true,
strictInputAccessModifiers: true
});
});
it('should produce diagnostics for inputs which assign to readonly, private, and protected fields',
() => {
env.write('test.ts', correctTypeInputsToRestrictedFields);
expectIllegalAssignmentErrors(env.driveDiagnostics());
});
it('should produce diagnostics for inputs which assign to readonly, private, and protected fields inherited from a base class',
() => {
env.write('test.ts', correctInputsToRestrictedFieldsFromBaseClass);
expectIllegalAssignmentErrors(env.driveDiagnostics());
});
function expectIllegalAssignmentErrors(diags: ReadonlyArray<ts.Diagnostic>) {
expect(diags.length).toBe(3);
const actualMessages = diags.map(d => d.messageText).sort();
const expectedMessages = [
`Property 'protectedField' is protected and only accessible within class 'TestDir' and its subclasses.`,
`Property 'privateField' is private and only accessible within class 'TestDir'.`,
`Cannot assign to 'readonlyField' because it is a read-only property.`,
].sort();
expect(actualMessages).toEqual(expectedMessages);
}
it('should report invalid type assignment when field name is not a valid JS identifier',
() => {
env.write('test.ts', `
import {Component, NgModule, Input, Directive} from '@angular/core';
@Component({
selector: 'blah',
template: '<div dir [private-input.xs]="value"></div>',
})
export class FooCmp {
value = 5;
}
@Directive({selector: '[dir]'})
export class TestDir {
@Input()
private 'private-input.xs'!: string;
}
@NgModule({
declarations: [FooCmp, TestDir],
})
export class FooModule {}
`); `);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(1);
expect(diags[0].messageText)
.toEqual(`Type 'number' is not assignable to type 'string'.`);
});
});
describe('with strict inputs', () => {
beforeEach(() => {
env.tsconfig({fullTemplateTypeCheck: true, strictInputTypes: true});
});
it('should not produce diagnostics for correct inputs which assign to readonly, private, or protected fields',
() => {
env.write('test.ts', correctTypeInputsToRestrictedFields);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(0);
});
it('should not produce diagnostics for correct inputs which assign to readonly, private, or protected fields inherited from a base class',
() => {
env.write('test.ts', correctInputsToRestrictedFieldsFromBaseClass);
const diags = env.driveDiagnostics(); const diags = env.driveDiagnostics();
expect(diags.length).toBe(0); expect(diags.length).toBe(0);
}); });