diff --git a/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/nullish_coalescing_not_nullable/BUILD.bazel b/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/nullish_coalescing_not_nullable/BUILD.bazel new file mode 100644 index 00000000000..30f1f8f4789 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/nullish_coalescing_not_nullable/BUILD.bazel @@ -0,0 +1,26 @@ +load("//tools:defaults.bzl", "jasmine_node_test", "ts_library") + +ts_library( + name = "test_lib", + testonly = True, + srcs = ["spec.ts"], + deps = [ + "//packages/compiler", + "//packages/compiler-cli/src/ngtsc/diagnostics", + "//packages/compiler-cli/src/ngtsc/file_system", + "//packages/compiler-cli/src/ngtsc/file_system/testing", + "//packages/compiler-cli/src/ngtsc/testing", + "//packages/compiler-cli/src/ngtsc/typecheck/extended", + "//packages/compiler-cli/src/ngtsc/typecheck/extended/checks/nullish_coalescing_not_nullable", + "//packages/compiler-cli/src/ngtsc/typecheck/testing", + "@npm//typescript", + ], +) + +jasmine_node_test( + name = "test", + bootstrap = ["//tools/testing:node_no_angular_es5"], + deps = [ + ":test_lib", + ], +) diff --git a/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/nullish_coalescing_not_nullable/spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/nullish_coalescing_not_nullable/spec.ts new file mode 100644 index 00000000000..3bed38765ad --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/nullish_coalescing_not_nullable/spec.ts @@ -0,0 +1,75 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import * as ts from 'typescript'; + +import {ErrorCode, ngErrorCode} from '../../../../../diagnostics'; +import {absoluteFrom, getSourceFileOrError} from '../../../../../file_system'; +import {runInEachFileSystem} from '../../../../../file_system/testing'; +import {getSourceCodeForDiagnostic} from '../../../../../testing'; +import {getClass, setup} from '../../../../testing'; +import {NullishCoalescingNotNullableCheck} from '../../../checks/nullish_coalescing_not_nullable'; +import {ExtendedTemplateCheckerImpl} from '../../../src/extended_template_checker'; + +runInEachFileSystem(() => { + describe('NullishCoalescingNotNullableCheck', () => { + it('should produce nullish coalescing warning', () => { + const fileName = absoluteFrom('/main.ts'); + const {program, templateTypeChecker} = setup([{ + fileName, + templates: { + 'TestCmp': `{{ var1 ?? 'foo' }}`, + }, + source: 'export class TestCmp { var1: string = "text"; }' + }]); + const sf = getSourceFileOrError(program, fileName); + const component = getClass(sf, 'TestCmp'); + const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( + templateTypeChecker, program.getTypeChecker(), [new NullishCoalescingNotNullableCheck()]); + const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); + expect(diags.length).toBe(1); + expect(diags[0].category).toBe(ts.DiagnosticCategory.Warning); + expect(diags[0].code).toBe(ngErrorCode(ErrorCode.NULLISH_COALESCING_NOT_NULLABLE)); + expect(getSourceCodeForDiagnostic(diags[0])).toBe(`var1 ?? 'foo'`); + }); + + it('should not produce nullish coalescing warning for a nullable type', () => { + const fileName = absoluteFrom('/main.ts'); + const {program, templateTypeChecker} = setup([{ + fileName, + templates: { + 'TestCmp': `{{ var1 ?? 'foo' }}`, + }, + source: 'export class TestCmp { var1: string | null = "text"; }' + }]); + const sf = getSourceFileOrError(program, fileName); + const component = getClass(sf, 'TestCmp'); + const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( + templateTypeChecker, program.getTypeChecker(), [new NullishCoalescingNotNullableCheck()]); + const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); + expect(diags.length).toBe(0); + }); + + it('should not produce nullish coalescing warning for a type that includes undefined', () => { + const fileName = absoluteFrom('/main.ts'); + const {program, templateTypeChecker} = setup([{ + fileName, + templates: { + 'TestCmp': `{{ var1 ?? 'foo' }}`, + }, + source: 'export class TestCmp { var1: string | undefined = "text"; }' + }]); + const sf = getSourceFileOrError(program, fileName); + const component = getClass(sf, 'TestCmp'); + const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( + templateTypeChecker, program.getTypeChecker(), [new NullishCoalescingNotNullableCheck()]); + const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); + expect(diags.length).toBe(0); + }); + }); +}); diff --git a/packages/compiler-cli/test/ngtsc/extended_template_diagnostics_spec.ts b/packages/compiler-cli/test/ngtsc/extended_template_diagnostics_spec.ts index 34681759bdc..e00e6fb3371 100644 --- a/packages/compiler-cli/test/ngtsc/extended_template_diagnostics_spec.ts +++ b/packages/compiler-cli/test/ngtsc/extended_template_diagnostics_spec.ts @@ -103,5 +103,43 @@ runInEachFileSystem(() => { .toMatch( /Error: The '_extendedTemplateDiagnostics' option requires 'strictTemplates' to also be enabled./); }); + + it(`should produce nullish coalescing not nullable warning`, () => { + env.write('test.ts', ` + import {Component} from '@angular/core'; + @Component({ + selector: 'test', + template: '{{ bar ?? "foo" }}', + }) + export class TestCmp { + bar: string = "text"; + } + `); + + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(1); + expect(diags[0].category).toBe(ts.DiagnosticCategory.Warning); + expect(diags[0].code).toBe(ngErrorCode(ErrorCode.NULLISH_COALESCING_NOT_NULLABLE)); + expect(getSourceCodeForDiagnostic(diags[0])).toBe('bar ?? "foo"'); + }); + + it(`should not produce nullish coalescing not nullable warning with strictNullChecks disabled`, + () => { + env.tsconfig( + {_extendedTemplateDiagnostics: true, strictTemplates: true, strictNullChecks: false}); + env.write('test.ts', ` + import {Component} from '@angular/core'; + @Component({ + selector: 'test', + template: '{{ bar ?? "foo" }}', + }) + export class TestCmp { + bar: string = undefined; + } + `); + + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(0); + }); }); });