Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ngtsc/ttc/strict #33365

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
48 changes: 40 additions & 8 deletions packages/compiler-cli/src/ngtsc/program.ts
Expand Up @@ -420,24 +420,29 @@ export class NgtscProgram implements api.Program {
// requested.
let typeCheckingConfig: TypeCheckingConfig;
if (this.options.fullTemplateTypeCheck) {
const strictTemplates = !!this.options.strictTemplates;
typeCheckingConfig = {
applyTemplateContextGuards: true,
checkQueries: false,
checkTemplateBodies: true,
checkTypeOfInputBindings: true,
strictNullInputBindings: true,
checkTypeOfInputBindings: strictTemplates,
strictNullInputBindings: strictTemplates,
checkTypeOfAttributes: strictTemplates,
// Even in full template type-checking mode, DOM binding checks are not quite ready yet.
checkTypeOfDomBindings: false,
checkTypeOfOutputEvents: true,
checkTypeOfAnimationEvents: true,
checkTypeOfOutputEvents: strictTemplates,
checkTypeOfAnimationEvents: strictTemplates,
// Checking of DOM events currently has an adverse effect on developer experience,
// e.g. for `<input (blur)="update($event.target.value)">` enabling this check results in:
// - error TS2531: Object is possibly 'null'.
// - error TS2339: Property 'value' does not exist on type 'EventTarget'.
checkTypeOfDomEvents: false,
checkTypeOfReferences: true,
checkTypeOfDomEvents: strictTemplates,
checkTypeOfDomReferences: strictTemplates,
// Non-DOM references have the correct type in View Engine so there is no strictness flag.
checkTypeOfNonDomReferences: true,
// Pipes are checked in View Engine so there is no strictness flag.
checkTypeOfPipes: true,
strictSafeNavigationTypes: true,
strictSafeNavigationTypes: strictTemplates,
};
} else {
typeCheckingConfig = {
Expand All @@ -446,16 +451,43 @@ export class NgtscProgram implements api.Program {
checkTemplateBodies: false,
checkTypeOfInputBindings: false,
strictNullInputBindings: false,
checkTypeOfAttributes: false,
checkTypeOfDomBindings: false,
checkTypeOfOutputEvents: false,
checkTypeOfAnimationEvents: false,
checkTypeOfDomEvents: false,
checkTypeOfReferences: false,
checkTypeOfDomReferences: false,
checkTypeOfNonDomReferences: false,
checkTypeOfPipes: false,
strictSafeNavigationTypes: false,
};
}

// Apply explicitly configured strictness flags on top of the default configuration
// based on "fullTemplateTypeCheck".
if (this.options.strictInputTypes !== undefined) {
typeCheckingConfig.checkTypeOfInputBindings = this.options.strictInputTypes;
}
if (this.options.strictNullInputTypes !== undefined) {
typeCheckingConfig.strictNullInputBindings = this.options.strictNullInputTypes;
}
if (this.options.strictOutputEventTypes !== undefined) {
typeCheckingConfig.checkTypeOfOutputEvents = this.options.strictOutputEventTypes;
typeCheckingConfig.checkTypeOfAnimationEvents = this.options.strictOutputEventTypes;
}
if (this.options.strictDomEventTypes !== undefined) {
typeCheckingConfig.checkTypeOfDomEvents = this.options.strictDomEventTypes;
}
if (this.options.strictSafeNavigationTypes !== undefined) {
typeCheckingConfig.strictSafeNavigationTypes = this.options.strictSafeNavigationTypes;
}
if (this.options.strictDomLocalRefTypes !== undefined) {
typeCheckingConfig.checkTypeOfDomReferences = this.options.strictDomLocalRefTypes;
}
if (this.options.strictAttributeTypes !== undefined) {
typeCheckingConfig.checkTypeOfAttributes = this.options.strictAttributeTypes;
}

// Execute the typeCheck phase of each decorator in the program.
const prepSpan = this.perfRecorder.start('typeCheckPrep');
const ctx = new TypeCheckContext(typeCheckingConfig, this.refEmitter !, this.typeCheckFilePath);
Expand Down
36 changes: 31 additions & 5 deletions packages/compiler-cli/src/ngtsc/typecheck/src/api.ts
Expand Up @@ -98,9 +98,24 @@ export interface TypeCheckingConfig {
* binding expressions are wrapped in a non-null assertion operator to effectively disable strict
* null checks. This may be particularly useful when the directive is from a library that is not
* compiled with `strictNullChecks` enabled.
*
* If `checkTypeOfInputBindings` is set to `false`, this flag has no effect.
*/
strictNullInputBindings: boolean;

/**
* Whether to check text attributes that happen to be consumed by a directive or component.
*
* For example, in a template containing `<input matInput disabled>` the `disabled` attribute ends
* up being consumed as an input with type `boolean` by the `matInput` directive. At runtime, the
* input will be set to the attribute's string value, which is an empty string for attributes
* without a value, so with this flag set to `true`, an error would be reported. If set to
* `false`, text attributes will never report an error.
*
* Note that if `checkTypeOfInputBindings` is set to `false`, this flag has no effect.
*/
checkTypeOfAttributes: boolean;

/**
* Whether to check the left-hand side type of binding operations to DOM properties.
*
Expand All @@ -114,7 +129,8 @@ export interface TypeCheckingConfig {
checkTypeOfDomBindings: boolean;

/**
* Whether to infer the type of the `$event` variable in event bindings for directive outputs.
* Whether to infer the type of the `$event` variable in event bindings for directive outputs or
* animation events.
*
* If this is `true`, the type of `$event` will be inferred based on the generic type of
* `EventEmitter`/`Subject` of the output. If set to `false`, the `$event` variable will be of
Expand All @@ -139,14 +155,24 @@ export interface TypeCheckingConfig {
*/
checkTypeOfDomEvents: boolean;

/**
* Whether to infer the type of local references to DOM elements.
*
* If this is `true`, the type of a `#ref` variable on a DOM node in the template will be
* determined by the type of `document.createElement` for the given DOM node type. If set to
* `false`, the type of `ref` for DOM nodes will be `any`.
*/
checkTypeOfDomReferences: boolean;


/**
* Whether to infer the type of local references.
*
* If this is `true`, the type of any `#ref` variable in the template will be determined by the
* referenced entity (either a directive or a DOM element). If set to `false`, the type of `ref`
* will be `any`.
* If this is `true`, the type of a `#ref` variable that points to a directive or `TemplateRef` in
* the template will be inferred correctly. If set to `false`, the type of `ref` for will be
* `any`.
*/
checkTypeOfReferences: boolean;
checkTypeOfNonDomReferences: boolean;

/**
* Whether to include type information from pipes in the type-checking operation.
Expand Down
25 changes: 20 additions & 5 deletions packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts
Expand Up @@ -1023,11 +1023,6 @@ class TcbExpressionTranslator {
addParseSpanInfo(expr, toAbsoluteSpan(ast.span, this.sourceSpan));
return expr;
} else if (binding instanceof TmplAstReference) {
if (!this.tcb.env.config.checkTypeOfReferences) {
// References are pinned to 'any'.
return NULL_AS_ANY;
}

const target = this.tcb.boundTarget.getReferenceTarget(binding);
if (target === null) {
throw new Error(`Unbound reference? ${binding.name}`);
Expand All @@ -1037,10 +1032,20 @@ class TcbExpressionTranslator {
// element or template.

if (target instanceof TmplAstElement) {
if (!this.tcb.env.config.checkTypeOfDomReferences) {
// References to DOM nodes are pinned to 'any'.
return NULL_AS_ANY;
}

const expr = ts.getMutableClone(this.scope.resolve(target));
addParseSpanInfo(expr, toAbsoluteSpan(ast.span, this.sourceSpan));
return expr;
} else if (target instanceof TmplAstTemplate) {
if (!this.tcb.env.config.checkTypeOfNonDomReferences) {
// References to `TemplateRef`s pinned to 'any'.
return NULL_AS_ANY;
}

// Direct references to an <ng-template> node simply require a value of type
// `TemplateRef<any>`. To get this, an expression of the form
// `(null as any as TemplateRef<any>)` is constructed.
Expand All @@ -1053,6 +1058,11 @@ class TcbExpressionTranslator {
addParseSpanInfo(value, toAbsoluteSpan(ast.span, this.sourceSpan));
return value;
} else {
if (!this.tcb.env.config.checkTypeOfNonDomReferences) {
// References to directives are pinned to 'any'.
return NULL_AS_ANY;
}

const expr = ts.getMutableClone(this.scope.resolve(target.node, target.directive));
addParseSpanInfo(expr, toAbsoluteSpan(ast.span, this.sourceSpan));
return expr;
Expand Down Expand Up @@ -1153,6 +1163,11 @@ function tcbGetDirectiveInputs(
return;
}

// Skip text attributes if configured to do so.
if (!tcb.env.config.checkTypeOfAttributes && attr instanceof TmplAstTextAttribute) {
return;
}

// Skip the attribute if the directive does not have an input for it.
if (!propMatch.has(attr.name)) {
return;
Expand Down
19 changes: 19 additions & 0 deletions packages/compiler-cli/src/ngtsc/typecheck/test/diagnostics_spec.ts
Expand Up @@ -154,6 +154,25 @@ runInEachFileSystem(() => {
]);
});

it('checks text attributes that are consumed by bindings with literal string types', () => {
const messages = diagnose(
`<div dir mode="drak"></div><div dir mode="light"></div>`, `
class Dir {
mode: 'dark'|'light';
}
class TestComponent {}`,
[{
type: 'directive',
name: 'Dir',
selector: '[dir]',
inputs: {'mode': 'mode'},
}]);

expect(messages).toEqual([
`synthetic.html(1, 10): Type '"drak"' is not assignable to type '"dark" | "light"'.`,
]);
});

it('produces diagnostics for pipes', () => {
const messages = diagnose(
`<div>{{ person.name | pipe:person.age:1 }}</div>`, `
Expand Down
8 changes: 6 additions & 2 deletions packages/compiler-cli/src/ngtsc/typecheck/test/test_utils.ts
Expand Up @@ -149,13 +149,15 @@ export const ALL_ENABLED_CONFIG: TypeCheckingConfig = {
checkTemplateBodies: true,
checkTypeOfInputBindings: true,
strictNullInputBindings: true,
checkTypeOfAttributes: true,
// Feature is still in development.
// TODO(alxhub): enable when DOM checking via lib.dom.d.ts is further along.
checkTypeOfDomBindings: false,
checkTypeOfOutputEvents: true,
checkTypeOfAnimationEvents: true,
checkTypeOfDomEvents: true,
checkTypeOfReferences: true,
checkTypeOfDomReferences: true,
checkTypeOfNonDomReferences: true,
checkTypeOfPipes: true,
strictSafeNavigationTypes: true,
};
Expand Down Expand Up @@ -201,11 +203,13 @@ export function tcb(
checkQueries: false,
checkTypeOfInputBindings: true,
strictNullInputBindings: true,
checkTypeOfAttributes: true,
checkTypeOfDomBindings: false,
checkTypeOfOutputEvents: true,
checkTypeOfAnimationEvents: true,
checkTypeOfDomEvents: true,
checkTypeOfReferences: true,
checkTypeOfDomReferences: true,
checkTypeOfNonDomReferences: true,
checkTypeOfPipes: true,
checkTemplateBodies: true,
strictSafeNavigationTypes: true,
Expand Down
Expand Up @@ -293,11 +293,13 @@ describe('type check blocks', () => {
checkTemplateBodies: true,
checkTypeOfInputBindings: true,
strictNullInputBindings: true,
checkTypeOfAttributes: true,
checkTypeOfDomBindings: false,
checkTypeOfOutputEvents: true,
checkTypeOfAnimationEvents: true,
checkTypeOfDomEvents: true,
checkTypeOfReferences: true,
checkTypeOfDomReferences: true,
checkTypeOfNonDomReferences: true,
checkTypeOfPipes: true,
strictSafeNavigationTypes: true,
};
Expand Down Expand Up @@ -423,7 +425,7 @@ describe('type check blocks', () => {
});
});

describe('config.checkTypeOfReferences', () => {
describe('config.checkTypeOfDomReferences', () => {
const TEMPLATE = `<input #ref>{{ref.value}}`;

it('should trace references when enabled', () => {
Expand All @@ -432,12 +434,69 @@ describe('type check blocks', () => {
});

it('should use any for reference types when disabled', () => {
const DISABLED_CONFIG: TypeCheckingConfig = {...BASE_CONFIG, checkTypeOfReferences: false};
const DISABLED_CONFIG:
TypeCheckingConfig = {...BASE_CONFIG, checkTypeOfDomReferences: false};
const block = tcb(TEMPLATE, [], DISABLED_CONFIG);
expect(block).toContain('(null as any).value');
});
});

describe('config.checkTypeOfNonDomReferences', () => {
const DIRECTIVES: TestDeclaration[] = [{
type: 'directive',
name: 'Dir',
selector: '[dir]',
exportAs: ['dir'],
inputs: {'dirInput': 'dirInput'},
outputs: {'outputField': 'dirOutput'},
hasNgTemplateContextGuard: true,
}];
const TEMPLATE =
`<div dir #ref="dir">{{ref.value}}</div><ng-template #ref2></ng-template>{{ref2.value2}}`;

it('should trace references to a directive when enabled', () => {
const block = tcb(TEMPLATE, DIRECTIVES);
expect(block).toContain('(_t2).value');
});

it('should trace references to an <ng-template> when enabled', () => {
const block = tcb(TEMPLATE, DIRECTIVES);
expect(block).toContain('((null as any as core.TemplateRef<any>)).value2');
});

it('should use any for reference types when disabled', () => {
const DISABLED_CONFIG:
TypeCheckingConfig = {...BASE_CONFIG, checkTypeOfNonDomReferences: false};
const block = tcb(TEMPLATE, DIRECTIVES, DISABLED_CONFIG);
expect(block).toContain('(null as any).value');
});
});

describe('config.checkTypeOfAttributes', () => {
const TEMPLATE = `<textarea dir disabled cols="3" [rows]="2">{{ref.value}}</textarea>`;
const DIRECTIVES: TestDeclaration[] = [{
type: 'directive',
name: 'Dir',
selector: '[dir]',
inputs: {'disabled': 'disabled', 'cols': 'cols', 'rows': 'rows'},
}];

it('should assign string value to the input when enabled', () => {
const block = tcb(TEMPLATE, DIRECTIVES);
expect(block).toContain('disabled: ("")');
expect(block).toContain('cols: ("3")');
expect(block).toContain('rows: (2)');
});

it('should use any for attributes but still check bound attributes when disabled', () => {
const DISABLED_CONFIG: TypeCheckingConfig = {...BASE_CONFIG, checkTypeOfAttributes: false};
const block = tcb(TEMPLATE, DIRECTIVES, DISABLED_CONFIG);
expect(block).toContain('disabled: (null as any)');
expect(block).toContain('cols: (null as any)');
expect(block).toContain('rows: (2)');
});
});

describe('config.checkTypeOfPipes', () => {
const TEMPLATE = `{{a | test:b:c}}`;
const PIPES: TestDeclaration[] = [{
Expand Down