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|
|-|-|
|`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`.

View File

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

View File

@ -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.
*

View File

@ -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;
}

View File

@ -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.
*/

View File

@ -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,
};
}

View File

@ -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,
};

View File

@ -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,

View File

@ -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.
*

View File

@ -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.

View File

@ -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 || {},

View File

@ -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)); ');
});
});
});
});

View File

@ -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);
});