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:
		
							parent
							
								
									fa0104017a
								
							
						
					
					
						commit
						71138f6004
					
				| @ -114,6 +114,7 @@ In case of a false positive like these, there are a few options: | ||||
| |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. | | ||||
| |`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.| | ||||
| |`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`. | ||||
|  | ||||
| @ -35,6 +35,7 @@ export interface StrictTemplateOptions { | ||||
|     strictContextGenerics?: boolean; | ||||
|     strictDomEventTypes?: boolean; | ||||
|     strictDomLocalRefTypes?: boolean; | ||||
|     strictInputAccessModifiers?: boolean; | ||||
|     strictInputTypes?: boolean; | ||||
|     strictLiteralTypes?: boolean; | ||||
|     strictNullInputTypes?: boolean; | ||||
|  | ||||
| @ -147,6 +147,18 @@ export interface StrictTemplateOptions { | ||||
|    */ | ||||
|   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. | ||||
|    * | ||||
|  | ||||
| @ -415,6 +415,7 @@ export class NgCompiler { | ||||
|         checkQueries: false, | ||||
|         checkTemplateBodies: true, | ||||
|         checkTypeOfInputBindings: strictTemplates, | ||||
|         honorAccessModifiersForInputBindings: false, | ||||
|         strictNullInputBindings: strictTemplates, | ||||
|         checkTypeOfAttributes: strictTemplates, | ||||
|         // Even in full template type-checking mode, DOM binding checks are not quite ready yet.
 | ||||
| @ -442,6 +443,7 @@ export class NgCompiler { | ||||
|         checkTemplateBodies: false, | ||||
|         checkTypeOfInputBindings: false, | ||||
|         strictNullInputBindings: false, | ||||
|         honorAccessModifiersForInputBindings: false, | ||||
|         checkTypeOfAttributes: false, | ||||
|         checkTypeOfDomBindings: false, | ||||
|         checkTypeOfOutputEvents: false, | ||||
| @ -462,6 +464,10 @@ export class NgCompiler { | ||||
|       typeCheckingConfig.checkTypeOfInputBindings = this.options.strictInputTypes; | ||||
|       typeCheckingConfig.applyTemplateContextGuards = this.options.strictInputTypes; | ||||
|     } | ||||
|     if (this.options.strictInputAccessModifiers !== undefined) { | ||||
|       typeCheckingConfig.honorAccessModifiersForInputBindings = | ||||
|           this.options.strictInputAccessModifiers; | ||||
|     } | ||||
|     if (this.options.strictNullInputTypes !== undefined) { | ||||
|       typeCheckingConfig.strictNullInputBindings = this.options.strictNullInputTypes; | ||||
|     } | ||||
|  | ||||
| @ -60,6 +60,13 @@ export interface DirectiveTypeCheckMeta { | ||||
|    */ | ||||
|   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. | ||||
|    */ | ||||
|  | ||||
| @ -27,9 +27,10 @@ export function flattenInheritedDirectiveMetadata( | ||||
| 
 | ||||
|   let inputs: {[key: string]: string|[string, string]} = {}; | ||||
|   let outputs: {[key: string]: string} = {}; | ||||
|   let coercedInputFields = new Set<string>(); | ||||
|   let undeclaredInputFields = new Set<string>(); | ||||
|   let restrictedInputFields = new Set<string>(); | ||||
|   const coercedInputFields = new Set<string>(); | ||||
|   const undeclaredInputFields = new Set<string>(); | ||||
|   const restrictedInputFields = new Set<string>(); | ||||
|   const stringLiteralInputFields = new Set<string>(); | ||||
|   let isDynamic = false; | ||||
| 
 | ||||
|   const addMetadata = (meta: DirectiveMeta): void => { | ||||
| @ -56,6 +57,9 @@ export function flattenInheritedDirectiveMetadata( | ||||
|     for (const restrictedInputField of meta.restrictedInputFields) { | ||||
|       restrictedInputFields.add(restrictedInputField); | ||||
|     } | ||||
|     for (const field of meta.stringLiteralInputFields) { | ||||
|       stringLiteralInputFields.add(field); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   addMetadata(topMeta); | ||||
| @ -67,6 +71,7 @@ export function flattenInheritedDirectiveMetadata( | ||||
|     coercedInputFields, | ||||
|     undeclaredInputFields, | ||||
|     restrictedInputFields, | ||||
|     stringLiteralInputFields, | ||||
|     baseClass: isDynamic ? 'dynamic' : null, | ||||
|   }; | ||||
| } | ||||
|  | ||||
| @ -98,15 +98,21 @@ export function extractDirectiveTypeCheckMeta( | ||||
|                   .filter((inputName): inputName is string => inputName !== null)); | ||||
| 
 | ||||
|   const restrictedInputFields = new Set<string>(); | ||||
|   const stringLiteralInputFields = new Set<string>(); | ||||
|   const undeclaredInputFields = new Set<string>(); | ||||
| 
 | ||||
|   for (const fieldName of Object.keys(inputs)) { | ||||
|     const field = members.find(member => member.name === fieldName); | ||||
|     if (field === undefined || field.node === null) { | ||||
|       undeclaredInputFields.add(fieldName); | ||||
|     } else if (isRestricted(field.node)) { | ||||
|       continue; | ||||
|     } | ||||
|     if (isRestricted(field.node)) { | ||||
|       restrictedInputFields.add(fieldName); | ||||
|     } | ||||
|     if (field.nameNode !== null && ts.isStringLiteral(field.nameNode)) { | ||||
|       stringLiteralInputFields.add(fieldName); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   const arity = reflector.getGenericArityOfClass(node); | ||||
| @ -116,6 +122,7 @@ export function extractDirectiveTypeCheckMeta( | ||||
|     ngTemplateGuards, | ||||
|     coercedInputFields, | ||||
|     restrictedInputFields, | ||||
|     stringLiteralInputFields, | ||||
|     undeclaredInputFields, | ||||
|     isGeneric: arity !== null && arity > 0, | ||||
|   }; | ||||
|  | ||||
| @ -244,6 +244,7 @@ function fakeDirective(ref: Reference<ClassDeclaration>): DirectiveMeta { | ||||
|     ngTemplateGuards: [], | ||||
|     coercedInputFields: new Set<string>(), | ||||
|     restrictedInputFields: new Set<string>(), | ||||
|     stringLiteralInputFields: new Set<string>(), | ||||
|     undeclaredInputFields: new Set<string>(), | ||||
|     isGeneric: false, | ||||
|     baseClass: null, | ||||
|  | ||||
| @ -90,6 +90,14 @@ export interface TypeCheckingConfig { | ||||
|    */ | ||||
|   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. | ||||
|    * | ||||
|  | ||||
| @ -453,8 +453,13 @@ class TcbDirectiveInputsOp extends TcbOp { | ||||
|           // declared in a `@Directive` or `@Component` decorator's `inputs` property) there is no
 | ||||
|           // assignment target available, so this field is skipped.
 | ||||
|           continue; | ||||
|         } else if (this.dir.restrictedInputFields.has(fieldName)) { | ||||
|           // To ignore errors, assign to temp variable with type of the field
 | ||||
|         } else if ( | ||||
|             !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 dirTypeRef = this.tcb.env.referenceType(this.dir.ref); | ||||
|           if (!ts.isTypeReferenceNode(dirTypeRef)) { | ||||
| @ -464,23 +469,16 @@ class TcbDirectiveInputsOp extends TcbOp { | ||||
|           const type = ts.createIndexedAccessTypeNode( | ||||
|               ts.createTypeQueryNode(dirId as ts.Identifier), | ||||
|               ts.createLiteralTypeNode(ts.createStringLiteral(fieldName))); | ||||
|           const temp = tsCreateVariable(id, ts.createNonNullExpression(ts.createNull()), type); | ||||
|           addParseSpanInfo(temp, input.attribute.sourceSpan); | ||||
|           const temp = tsDeclareVariable(id, type); | ||||
|           this.scope.addStatement(temp); | ||||
|           target = id; | ||||
| 
 | ||||
|           // TODO: To get errors assign directly to the fields on the instance, using dot access
 | ||||
|           // when possible
 | ||||
| 
 | ||||
|         } else { | ||||
|           // Otherwise, a declaration exists in which case the `dir["fieldName"]` syntax is used
 | ||||
|           // as assignment target. An element access is used instead of a property access to
 | ||||
|           // support input names that are not valid JavaScript identifiers. Additionally, using
 | ||||
|           // element access syntax does not produce
 | ||||
|           // TS2341 "Property $prop is private and only accessible within class $class." nor
 | ||||
|           // TS2445 "Property $prop is protected and only accessible within class $class and its
 | ||||
|           //         subclasses."
 | ||||
|           target = ts.createElementAccess(dirId, ts.createStringLiteral(fieldName)); | ||||
|           // To get errors assign directly to the fields on the instance, using property access
 | ||||
|           // when possible. String literal fields may not be valid JS identifiers so we use
 | ||||
|           // literal element access instead for those cases.
 | ||||
|           target = this.dir.stringLiteralInputFields.has(fieldName) ? | ||||
|               ts.createElementAccess(dirId, ts.createStringLiteral(fieldName)) : | ||||
|               ts.createPropertyAccess(dirId, ts.createIdentifier(fieldName)); | ||||
|         } | ||||
| 
 | ||||
|         // Finally the assignment is extended by assigning it into the target expression.
 | ||||
|  | ||||
| @ -157,6 +157,7 @@ export const ALL_ENABLED_CONFIG: TypeCheckingConfig = { | ||||
|   checkQueries: false, | ||||
|   checkTemplateBodies: true, | ||||
|   checkTypeOfInputBindings: true, | ||||
|   honorAccessModifiersForInputBindings: true, | ||||
|   strictNullInputBindings: true, | ||||
|   checkTypeOfAttributes: true, | ||||
|   // Feature is still in development.
 | ||||
| @ -178,10 +179,11 @@ export type TestDirective = Partial<Pick< | ||||
|     TypeCheckableDirectiveMeta, | ||||
|     Exclude< | ||||
|         keyof TypeCheckableDirectiveMeta, | ||||
|         'ref'|'coercedInputFields'|'restrictedInputFields'|'undeclaredInputFields'>>>&{ | ||||
|         'ref'|'coercedInputFields'|'restrictedInputFields'|'stringLiteralInputFields'| | ||||
|         'undeclaredInputFields'>>>&{ | ||||
|   selector: string, name: string, file?: AbsoluteFsPath, type: 'directive', | ||||
|       coercedInputFields?: string[], restrictedInputFields?: string[], | ||||
|       undeclaredInputFields?: string[], isGeneric?: boolean; | ||||
|       stringLiteralInputFields?: string[], undeclaredInputFields?: string[], isGeneric?: boolean; | ||||
| }; | ||||
| export type TestPipe = { | ||||
|   name: string, | ||||
| @ -212,6 +214,7 @@ export function tcb( | ||||
|     applyTemplateContextGuards: true, | ||||
|     checkQueries: false, | ||||
|     checkTypeOfInputBindings: true, | ||||
|     honorAccessModifiersForInputBindings: false, | ||||
|     strictNullInputBindings: true, | ||||
|     checkTypeOfAttributes: true, | ||||
|     checkTypeOfDomBindings: false, | ||||
| @ -420,6 +423,7 @@ function prepareDeclarations( | ||||
|       ngTemplateGuards: decl.ngTemplateGuards || [], | ||||
|       coercedInputFields: new Set<string>(decl.coercedInputFields || []), | ||||
|       restrictedInputFields: new Set<string>(decl.restrictedInputFields || []), | ||||
|       stringLiteralInputFields: new Set<string>(decl.stringLiteralInputFields || []), | ||||
|       undeclaredInputFields: new Set<string>(decl.undeclaredInputFields || []), | ||||
|       isGeneric: decl.isGeneric ?? false, | ||||
|       outputs: decl.outputs || {}, | ||||
|  | ||||
| @ -55,7 +55,7 @@ describe('type check blocks', () => { | ||||
|       selector: '[dir]', | ||||
|       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', () => { | ||||
| @ -67,8 +67,8 @@ describe('type check blocks', () => { | ||||
|       inputs: {inputA: 'inputA'}, | ||||
|     }]; | ||||
|     const block = tcb(TEMPLATE, DIRECTIVES); | ||||
|     expect(block).toContain('_t2["inputA"] = (1);'); | ||||
|     expect(block).toContain('_t2["inputA"] = (2);'); | ||||
|     expect(block).toContain('_t2.inputA = (1);'); | ||||
|     expect(block).toContain('_t2.inputA = (2);'); | ||||
|   }); | ||||
| 
 | ||||
|   it('should handle empty bindings', () => { | ||||
| @ -79,7 +79,7 @@ describe('type check blocks', () => { | ||||
|       selector: '[dir-a]', | ||||
|       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', () => { | ||||
| @ -90,7 +90,7 @@ describe('type check blocks', () => { | ||||
|       selector: '[dir-a]', | ||||
|       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', () => { | ||||
| @ -322,7 +322,7 @@ describe('type check blocks', () => { | ||||
|     expect(tcb(TEMPLATE, DIRECTIVES)) | ||||
|         .toContain( | ||||
|             'var _t2: Dir = (null!); ' + | ||||
|             '_t2["input"] = (_t2);'); | ||||
|             '_t2.input = (_t2);'); | ||||
|   }); | ||||
| 
 | ||||
|   it('should generate circular references between two directives correctly', () => { | ||||
| @ -350,9 +350,9 @@ describe('type check blocks', () => { | ||||
|         .toContain( | ||||
|             'var _t2: DirA = (null!); ' + | ||||
|             'var _t3: DirB = (null!); ' + | ||||
|             '_t2["inputA"] = (_t3); ' + | ||||
|             '_t2.inputA = (_t3); ' + | ||||
|             'var _t4 = document.createElement("div"); ' + | ||||
|             '_t3["inputA"] = (_t2);'); | ||||
|             '_t3.inputA = (_t2);'); | ||||
|   }); | ||||
| 
 | ||||
|   it('should handle undeclared properties', () => { | ||||
| @ -372,7 +372,7 @@ describe('type check blocks', () => { | ||||
|             '(((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 DIRECTIVES: TestDeclaration[] = [{ | ||||
|       type: 'directive', | ||||
| @ -390,6 +390,24 @@ describe('type check blocks', () => { | ||||
|             '_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', () => { | ||||
|     const TEMPLATE = `<div dir [inputA]="foo"></div>`; | ||||
|     const DIRECTIVES: TestDeclaration[] = [{ | ||||
| @ -404,7 +422,7 @@ describe('type check blocks', () => { | ||||
|     expect(tcb(TEMPLATE, DIRECTIVES)) | ||||
|         .toContain( | ||||
|             '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', | ||||
| @ -424,7 +442,7 @@ describe('type check blocks', () => { | ||||
|            .toContain( | ||||
|                'var _t2: Dir = (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', | ||||
| @ -443,7 +461,7 @@ describe('type check blocks', () => { | ||||
|        expect(tcb(TEMPLATE, DIRECTIVES)) | ||||
|            .toContain( | ||||
|                'var _t2: Dir = (null!); ' + | ||||
|                '_t2["field2"] = (((ctx).foo));'); | ||||
|                '_t2.field2 = (((ctx).foo));'); | ||||
|      }); | ||||
| 
 | ||||
|   it('should use coercion types if declared', () => { | ||||
| @ -590,6 +608,7 @@ describe('type check blocks', () => { | ||||
|       checkQueries: false, | ||||
|       checkTemplateBodies: true, | ||||
|       checkTypeOfInputBindings: true, | ||||
|       honorAccessModifiersForInputBindings: false, | ||||
|       strictNullInputBindings: true, | ||||
|       checkTypeOfAttributes: true, | ||||
|       checkTypeOfDomBindings: false, | ||||
| @ -639,14 +658,14 @@ describe('type check blocks', () => { | ||||
| 
 | ||||
|       it('should include null and undefined when enabled', () => { | ||||
|         const block = tcb(TEMPLATE, DIRECTIVES); | ||||
|         expect(block).toContain('_t2["dirInput"] = (((ctx).a));'); | ||||
|         expect(block).toContain('_t2.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('_t2["dirInput"] = (((ctx).a)!);'); | ||||
|         expect(block).toContain('_t2.dirInput = (((ctx).a)!);'); | ||||
|         expect(block).toContain('((ctx).b)!;'); | ||||
|       }); | ||||
|     }); | ||||
| @ -655,7 +674,7 @@ 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('_t2["dirInput"] = (((ctx).a));'); | ||||
|         expect(block).toContain('_t2.dirInput = (((ctx).a));'); | ||||
|         expect(block).toContain('((ctx).b);'); | ||||
|       }); | ||||
| 
 | ||||
| @ -664,7 +683,7 @@ describe('type check blocks', () => { | ||||
|         const DISABLED_CONFIG: | ||||
|             TypeCheckingConfig = {...BASE_CONFIG, checkTypeOfInputBindings: false}; | ||||
|         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);'); | ||||
|       }); | ||||
| 
 | ||||
| @ -673,7 +692,7 @@ describe('type check blocks', () => { | ||||
|         const DISABLED_CONFIG: | ||||
|             TypeCheckingConfig = {...BASE_CONFIG, checkTypeOfInputBindings: false}; | ||||
|         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', () => { | ||||
|         const block = tcb(TEMPLATE, DIRECTIVES); | ||||
|         expect(block).toContain('_t2["disabled"] = ("");'); | ||||
|         expect(block).toContain('_t2["cols"] = ("3");'); | ||||
|         expect(block).toContain('_t2["rows"] = (2);'); | ||||
|         expect(block).toContain('_t2.disabled = ("");'); | ||||
|         expect(block).toContain('_t2.cols = ("3");'); | ||||
|         expect(block).toContain('_t2.rows = (2);'); | ||||
|       }); | ||||
| 
 | ||||
|       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); | ||||
|         expect(block).not.toContain('"disabled"'); | ||||
|         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>)'); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     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)); '); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| @ -1577,14 +1577,7 @@ export declare class AnimationEvent { | ||||
|             } | ||||
|       `;
 | ||||
| 
 | ||||
|       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', ` | ||||
|       const correctTypeInputsToRestrictedFields = ` | ||||
|             import {Component, NgModule, Input, Directive} from '@angular/core'; | ||||
| 
 | ||||
|             @Component({ | ||||
| @ -1601,14 +1594,9 @@ export declare class AnimationEvent { | ||||
|               declarations: [FooCmp, TestDir], | ||||
|             }) | ||||
|             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', | ||||
|            () => { | ||||
|              env.write('test.ts', ` | ||||
|       const correctInputsToRestrictedFieldsFromBaseClass = ` | ||||
|             import {Component, NgModule, Input, Directive} from '@angular/core'; | ||||
| 
 | ||||
|             @Component({ | ||||
| @ -1629,7 +1617,85 @@ export declare class AnimationEvent { | ||||
|               declarations: [FooCmp, ChildDir], | ||||
|             }) | ||||
|             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(); | ||||
|              expect(diags.length).toBe(0); | ||||
|            }); | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user