Skip to content

Commit 71138f6

Browse files
atscottAndrewKushnir
authored andcommitted
feat(compiler-cli): Add compiler option to report errors when assigning to restricted input fields (angular#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 angular#38249
1 parent fa01040 commit 71138f6

File tree

13 files changed

+236
-59
lines changed

13 files changed

+236
-59
lines changed

aio/content/guide/template-typecheck.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ In case of a false positive like these, there are a few options:
114114
|Strictness flag|Effect|
115115
|-|-|
116116
|`strictInputTypes`|Whether the assignability of a binding expression to the `@Input()` field is checked. Also affects the inference of directive generic types. |
117+
|`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.|
117118
|`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.|
118119
|`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'">`).
119120
|`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`.

goldens/public-api/compiler-cli/compiler_options.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export interface StrictTemplateOptions {
3535
strictContextGenerics?: boolean;
3636
strictDomEventTypes?: boolean;
3737
strictDomLocalRefTypes?: boolean;
38+
strictInputAccessModifiers?: boolean;
3839
strictInputTypes?: boolean;
3940
strictLiteralTypes?: boolean;
4041
strictNullInputTypes?: boolean;

packages/compiler-cli/src/ngtsc/core/api/src/public_options.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,18 @@ export interface StrictTemplateOptions {
147147
*/
148148
strictInputTypes?: boolean;
149149

150+
/**
151+
* Whether to check if the input binding attempts to assign to a restricted field (readonly,
152+
* private, or protected) on the directive/component.
153+
*
154+
* Defaults to `false`, even if "fullTemplateTypeCheck", "strictTemplates" and/or
155+
* "strictInputTypes" is set. Note that if `strictInputTypes` is not set, or set to `false`, this
156+
* flag has no effect.
157+
*
158+
* Tracking issue for enabling this by default: https://github.com/angular/angular/issues/38400
159+
*/
160+
strictInputAccessModifiers?: boolean;
161+
150162
/**
151163
* Whether to use strict null types for input bindings for directives.
152164
*

packages/compiler-cli/src/ngtsc/core/src/compiler.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,7 @@ export class NgCompiler {
415415
checkQueries: false,
416416
checkTemplateBodies: true,
417417
checkTypeOfInputBindings: strictTemplates,
418+
honorAccessModifiersForInputBindings: false,
418419
strictNullInputBindings: strictTemplates,
419420
checkTypeOfAttributes: strictTemplates,
420421
// Even in full template type-checking mode, DOM binding checks are not quite ready yet.
@@ -442,6 +443,7 @@ export class NgCompiler {
442443
checkTemplateBodies: false,
443444
checkTypeOfInputBindings: false,
444445
strictNullInputBindings: false,
446+
honorAccessModifiersForInputBindings: false,
445447
checkTypeOfAttributes: false,
446448
checkTypeOfDomBindings: false,
447449
checkTypeOfOutputEvents: false,
@@ -462,6 +464,10 @@ export class NgCompiler {
462464
typeCheckingConfig.checkTypeOfInputBindings = this.options.strictInputTypes;
463465
typeCheckingConfig.applyTemplateContextGuards = this.options.strictInputTypes;
464466
}
467+
if (this.options.strictInputAccessModifiers !== undefined) {
468+
typeCheckingConfig.honorAccessModifiersForInputBindings =
469+
this.options.strictInputAccessModifiers;
470+
}
465471
if (this.options.strictNullInputTypes !== undefined) {
466472
typeCheckingConfig.strictNullInputBindings = this.options.strictNullInputTypes;
467473
}

packages/compiler-cli/src/ngtsc/metadata/src/api.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,13 @@ export interface DirectiveTypeCheckMeta {
6060
*/
6161
restrictedInputFields: Set<string>;
6262

63+
/**
64+
* The set of input fields which are declared as string literal members in the Directive's class.
65+
* We need to track these separately because these fields may not be valid JS identifiers so
66+
* we cannot use them with property access expressions when assigning inputs.
67+
*/
68+
stringLiteralInputFields: Set<string>;
69+
6370
/**
6471
* The set of input fields which do not have corresponding members in the Directive's class.
6572
*/

packages/compiler-cli/src/ngtsc/metadata/src/inheritance.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,10 @@ export function flattenInheritedDirectiveMetadata(
2727

2828
let inputs: {[key: string]: string|[string, string]} = {};
2929
let outputs: {[key: string]: string} = {};
30-
let coercedInputFields = new Set<string>();
31-
let undeclaredInputFields = new Set<string>();
32-
let restrictedInputFields = new Set<string>();
30+
const coercedInputFields = new Set<string>();
31+
const undeclaredInputFields = new Set<string>();
32+
const restrictedInputFields = new Set<string>();
33+
const stringLiteralInputFields = new Set<string>();
3334
let isDynamic = false;
3435

3536
const addMetadata = (meta: DirectiveMeta): void => {
@@ -56,6 +57,9 @@ export function flattenInheritedDirectiveMetadata(
5657
for (const restrictedInputField of meta.restrictedInputFields) {
5758
restrictedInputFields.add(restrictedInputField);
5859
}
60+
for (const field of meta.stringLiteralInputFields) {
61+
stringLiteralInputFields.add(field);
62+
}
5963
};
6064

6165
addMetadata(topMeta);
@@ -67,6 +71,7 @@ export function flattenInheritedDirectiveMetadata(
6771
coercedInputFields,
6872
undeclaredInputFields,
6973
restrictedInputFields,
74+
stringLiteralInputFields,
7075
baseClass: isDynamic ? 'dynamic' : null,
7176
};
7277
}

packages/compiler-cli/src/ngtsc/metadata/src/util.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,15 +98,21 @@ export function extractDirectiveTypeCheckMeta(
9898
.filter((inputName): inputName is string => inputName !== null));
9999

100100
const restrictedInputFields = new Set<string>();
101+
const stringLiteralInputFields = new Set<string>();
101102
const undeclaredInputFields = new Set<string>();
102103

103104
for (const fieldName of Object.keys(inputs)) {
104105
const field = members.find(member => member.name === fieldName);
105106
if (field === undefined || field.node === null) {
106107
undeclaredInputFields.add(fieldName);
107-
} else if (isRestricted(field.node)) {
108+
continue;
109+
}
110+
if (isRestricted(field.node)) {
108111
restrictedInputFields.add(fieldName);
109112
}
113+
if (field.nameNode !== null && ts.isStringLiteral(field.nameNode)) {
114+
stringLiteralInputFields.add(fieldName);
115+
}
110116
}
111117

112118
const arity = reflector.getGenericArityOfClass(node);
@@ -116,6 +122,7 @@ export function extractDirectiveTypeCheckMeta(
116122
ngTemplateGuards,
117123
coercedInputFields,
118124
restrictedInputFields,
125+
stringLiteralInputFields,
119126
undeclaredInputFields,
120127
isGeneric: arity !== null && arity > 0,
121128
};

packages/compiler-cli/src/ngtsc/scope/test/local_spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@ function fakeDirective(ref: Reference<ClassDeclaration>): DirectiveMeta {
244244
ngTemplateGuards: [],
245245
coercedInputFields: new Set<string>(),
246246
restrictedInputFields: new Set<string>(),
247+
stringLiteralInputFields: new Set<string>(),
247248
undeclaredInputFields: new Set<string>(),
248249
isGeneric: false,
249250
baseClass: null,

packages/compiler-cli/src/ngtsc/typecheck/api/api.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,14 @@ export interface TypeCheckingConfig {
9090
*/
9191
checkTypeOfInputBindings: boolean;
9292

93+
/**
94+
* Whether to honor the access modifiers on input bindings for the component/directive.
95+
*
96+
* If a template binding attempts to assign to an input that is private/protected/readonly,
97+
* this will produce errors when enabled but will not when disabled.
98+
*/
99+
honorAccessModifiersForInputBindings: boolean;
100+
93101
/**
94102
* Whether to use strict null types for input bindings for directives.
95103
*

packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -453,8 +453,13 @@ class TcbDirectiveInputsOp extends TcbOp {
453453
// declared in a `@Directive` or `@Component` decorator's `inputs` property) there is no
454454
// assignment target available, so this field is skipped.
455455
continue;
456-
} else if (this.dir.restrictedInputFields.has(fieldName)) {
457-
// To ignore errors, assign to temp variable with type of the field
456+
} else if (
457+
!this.tcb.env.config.honorAccessModifiersForInputBindings &&
458+
this.dir.restrictedInputFields.has(fieldName)) {
459+
// If strict checking of access modifiers is disabled and the field is restricted
460+
// (i.e. private/protected/readonly), generate an assignment into a temporary variable
461+
// that has the type of the field. This achieves type-checking but circumvents the access
462+
// modifiers.
458463
const id = this.tcb.allocateId();
459464
const dirTypeRef = this.tcb.env.referenceType(this.dir.ref);
460465
if (!ts.isTypeReferenceNode(dirTypeRef)) {
@@ -464,23 +469,16 @@ class TcbDirectiveInputsOp extends TcbOp {
464469
const type = ts.createIndexedAccessTypeNode(
465470
ts.createTypeQueryNode(dirId as ts.Identifier),
466471
ts.createLiteralTypeNode(ts.createStringLiteral(fieldName)));
467-
const temp = tsCreateVariable(id, ts.createNonNullExpression(ts.createNull()), type);
468-
addParseSpanInfo(temp, input.attribute.sourceSpan);
472+
const temp = tsDeclareVariable(id, type);
469473
this.scope.addStatement(temp);
470474
target = id;
471-
472-
// TODO: To get errors assign directly to the fields on the instance, using dot access
473-
// when possible
474-
475475
} else {
476-
// Otherwise, a declaration exists in which case the `dir["fieldName"]` syntax is used
477-
// as assignment target. An element access is used instead of a property access to
478-
// support input names that are not valid JavaScript identifiers. Additionally, using
479-
// element access syntax does not produce
480-
// TS2341 "Property $prop is private and only accessible within class $class." nor
481-
// TS2445 "Property $prop is protected and only accessible within class $class and its
482-
// subclasses."
483-
target = ts.createElementAccess(dirId, ts.createStringLiteral(fieldName));
476+
// To get errors assign directly to the fields on the instance, using property access
477+
// when possible. String literal fields may not be valid JS identifiers so we use
478+
// literal element access instead for those cases.
479+
target = this.dir.stringLiteralInputFields.has(fieldName) ?
480+
ts.createElementAccess(dirId, ts.createStringLiteral(fieldName)) :
481+
ts.createPropertyAccess(dirId, ts.createIdentifier(fieldName));
484482
}
485483

486484
// Finally the assignment is extended by assigning it into the target expression.

packages/compiler-cli/src/ngtsc/typecheck/test/test_utils.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ export const ALL_ENABLED_CONFIG: TypeCheckingConfig = {
157157
checkQueries: false,
158158
checkTemplateBodies: true,
159159
checkTypeOfInputBindings: true,
160+
honorAccessModifiersForInputBindings: true,
160161
strictNullInputBindings: true,
161162
checkTypeOfAttributes: true,
162163
// Feature is still in development.
@@ -178,10 +179,11 @@ export type TestDirective = Partial<Pick<
178179
TypeCheckableDirectiveMeta,
179180
Exclude<
180181
keyof TypeCheckableDirectiveMeta,
181-
'ref'|'coercedInputFields'|'restrictedInputFields'|'undeclaredInputFields'>>>&{
182+
'ref'|'coercedInputFields'|'restrictedInputFields'|'stringLiteralInputFields'|
183+
'undeclaredInputFields'>>>&{
182184
selector: string, name: string, file?: AbsoluteFsPath, type: 'directive',
183185
coercedInputFields?: string[], restrictedInputFields?: string[],
184-
undeclaredInputFields?: string[], isGeneric?: boolean;
186+
stringLiteralInputFields?: string[], undeclaredInputFields?: string[], isGeneric?: boolean;
185187
};
186188
export type TestPipe = {
187189
name: string,
@@ -212,6 +214,7 @@ export function tcb(
212214
applyTemplateContextGuards: true,
213215
checkQueries: false,
214216
checkTypeOfInputBindings: true,
217+
honorAccessModifiersForInputBindings: false,
215218
strictNullInputBindings: true,
216219
checkTypeOfAttributes: true,
217220
checkTypeOfDomBindings: false,
@@ -420,6 +423,7 @@ function prepareDeclarations(
420423
ngTemplateGuards: decl.ngTemplateGuards || [],
421424
coercedInputFields: new Set<string>(decl.coercedInputFields || []),
422425
restrictedInputFields: new Set<string>(decl.restrictedInputFields || []),
426+
stringLiteralInputFields: new Set<string>(decl.stringLiteralInputFields || []),
423427
undeclaredInputFields: new Set<string>(decl.undeclaredInputFields || []),
424428
isGeneric: decl.isGeneric ?? false,
425429
outputs: decl.outputs || {},

0 commit comments

Comments
 (0)