diff --git a/packages/compiler-cli/src/ngtsc/typecheck/extended/BUILD.bazel b/packages/compiler-cli/src/ngtsc/typecheck/extended/BUILD.bazel new file mode 100644 index 0000000000..ca8d5b1c9f --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/typecheck/extended/BUILD.bazel @@ -0,0 +1,15 @@ +load("//tools:defaults.bzl", "ts_library") + +ts_library( + name = "extended", + srcs = glob( + ["**/*.ts"], + ), + visibility = ["//packages/compiler-cli/src/ngtsc:__subpackages__"], + deps = [ + "//packages/compiler-cli/src/ngtsc/diagnostics", + "//packages/compiler-cli/src/ngtsc/typecheck/api", + "//packages/compiler-cli/src/ngtsc/typecheck/extended/api", + "@npm//typescript", + ], +) diff --git a/packages/compiler-cli/src/ngtsc/typecheck/extended/src/template_checker.ts b/packages/compiler-cli/src/ngtsc/typecheck/extended/src/template_checker.ts new file mode 100644 index 0000000000..f92e424216 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/typecheck/extended/src/template_checker.ts @@ -0,0 +1,92 @@ +/** + * @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} from '../../../diagnostics'; +import {TemplateTypeChecker} from '../../api'; +import {TemplateCheck, TemplateContext} from '../api/api'; + +/** + * Run all `TemplateChecks` for a component and return the generated `ts.Diagnostic`s. + * @param component the `@Component()` class from which the template is obtained + * @param templateTypeChecker interface to get information about template nodes + * @param typeChecker program's type checker + * @param templateChecks specific checks to be run + * @returns generated `ts.Diagnostic[]` + */ +export function getExtendedTemplateDiagnosticsForComponent( + component: ts.ClassDeclaration, templateTypeChecker: TemplateTypeChecker, + typeChecker: ts.TypeChecker, templateChecks: TemplateCheck[]): ts.Diagnostic[] { + const template = templateTypeChecker.getTemplate(component); + // Skip checks if component has no template. This can happen if the user writes a + // `@Component()` but doesn't add the template, could happen in the language service + // when users are in the middle of typing code. + if (template === null) { + return []; + } + const diagnostics: ts.Diagnostic[] = []; + + const ctx = {templateTypeChecker, typeChecker, component} as TemplateContext; + + for (const check of templateChecks) { + diagnostics.push(...deduplicateDiagnostics(check.run(ctx, template))); + } + + return diagnostics; +} + +// Filter out duplicated diagnostics, this is possible due to the way the compiler +// handles desugaring and produces `AST`s. Ex. +// +// ``` +//
test
+// ``` +// +// Would result in the following AST: +// +// ``` +// Template { +// outputs: [ +// BoundEvent { +// name: 'foo', +// /.../ +// } +// ], +// children: [ +// Element { +// outputs: [ +// BoundEvent { +// name: 'foo', +// /.../ +// } +// ] +// } +// ], +// /.../ +// } +// ``` +// +// In this case a duplicated diagnostic could be generated for the output `foo`. +// TODO(danieltrevino): handle duplicated diagnostics when they are being generated +// to avoid extra work (could be directly in the visitor). +// https://github.com/angular/angular/pull/42984#discussion_r684823926 +function deduplicateDiagnostics(diagnostics: ts.Diagnostic[]): ts.Diagnostic[] { + const result: ts.Diagnostic[] = []; + for (const newDiag of diagnostics) { + const isDuplicateDiag = result.some(existingDiag => areDiagnosticsEqual(newDiag, existingDiag)); + if (!isDuplicateDiag) { + result.push(newDiag); + } + } + return result; +} + +function areDiagnosticsEqual(first: ts.Diagnostic, second: ts.Diagnostic): boolean { + return first.file?.fileName === second.file?.fileName && first.start === second.start && + first.length === second.length && first.code === second.code; +}