diff --git a/packages/compiler-cli/src/ngtsc/core/BUILD.bazel b/packages/compiler-cli/src/ngtsc/core/BUILD.bazel index 658f85d43a1..93338ba10c5 100644 --- a/packages/compiler-cli/src/ngtsc/core/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/core/BUILD.bazel @@ -42,6 +42,7 @@ ts_library( "//packages/compiler-cli/src/ngtsc/typecheck/template_semantics", "//packages/compiler-cli/src/ngtsc/typecheck/template_semantics/api", "//packages/compiler-cli/src/ngtsc/util", + "//packages/compiler-cli/src/ngtsc/validation", "//packages/compiler-cli/src/ngtsc/xi18n", "@npm//@types/semver", "@npm//semver", diff --git a/packages/compiler-cli/src/ngtsc/core/src/compiler.ts b/packages/compiler-cli/src/ngtsc/core/src/compiler.ts index 660aa162c65..a1030f81201 100644 --- a/packages/compiler-cli/src/ngtsc/core/src/compiler.ts +++ b/packages/compiler-cli/src/ngtsc/core/src/compiler.ts @@ -37,6 +37,7 @@ import {ExtendedTemplateChecker} from '../../typecheck/extended/api'; import {TemplateSemanticsChecker} from '../../typecheck/template_semantics/api/api'; import {TemplateSemanticsCheckerImpl} from '../../typecheck/template_semantics/src/template_semantics_checker'; import {getSourceFileOrNull, isDtsPath, toUnredirectedSourceFile} from '../../util/src/typescript'; +import {SourceFileValidator} from '../../validation'; import {Xi18nContext} from '../../xi18n'; import {DiagnosticCategoryLabel, NgCompilerAdapter, NgCompilerOptions} from '../api'; @@ -62,6 +63,7 @@ interface LazyCompilationState { resourceRegistry: ResourceRegistry; extendedTemplateChecker: ExtendedTemplateChecker|null; templateSemanticsChecker: TemplateSemanticsChecker|null; + sourceFileValidator: SourceFileValidator|null; /** * Only available in local compilation mode when option `generateExtraImportsInLocalMode` is set. @@ -978,10 +980,17 @@ export class NgCompiler { private runAdditionalChecks(sf?: ts.SourceFile): ts.Diagnostic[] { const diagnostics: ts.Diagnostic[] = []; const compilation = this.ensureAnalyzed(); - const {extendedTemplateChecker, templateSemanticsChecker} = compilation; + const {extendedTemplateChecker, templateSemanticsChecker, sourceFileValidator} = compilation; const files = sf ? [sf] : this.inputProgram.getSourceFiles(); for (const sf of files) { + if (sourceFileValidator !== null) { + const sourceFileDiagnostics = sourceFileValidator.getDiagnosticsForFile(sf); + if (sourceFileDiagnostics !== null) { + diagnostics.push(...sourceFileDiagnostics); + } + } + if (templateSemanticsChecker !== null) { diagnostics.push(...compilation.traitCompiler.runAdditionalChecks(sf, (clazz, handler) => { return handler.templateSemanticsCheck?.(clazz, templateSemanticsChecker) || null; @@ -1239,6 +1248,10 @@ export class NgCompiler { new TemplateSemanticsCheckerImpl(templateTypeChecker) : null; + const sourceFileValidator = this.constructionDiagnostics.length === 0 ? + new SourceFileValidator(reflector, importTracker) : + null; + return { isCore, traitCompiler, @@ -1255,6 +1268,7 @@ export class NgCompiler { extendedTemplateChecker, localCompilationExtraImportsTracker, templateSemanticsChecker, + sourceFileValidator, }; } } diff --git a/packages/compiler-cli/src/ngtsc/validation/BUILD.bazel b/packages/compiler-cli/src/ngtsc/validation/BUILD.bazel new file mode 100644 index 00000000000..7a201566e68 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/validation/BUILD.bazel @@ -0,0 +1,18 @@ +load("//tools:defaults.bzl", "ts_library") + +package(default_visibility = ["//visibility:public"]) + +ts_library( + name = "validation", + srcs = glob( + ["**/*.ts"], + ), + deps = [ + "//packages/compiler-cli/src/ngtsc/annotations", + "//packages/compiler-cli/src/ngtsc/diagnostics", + "//packages/compiler-cli/src/ngtsc/imports", + "//packages/compiler-cli/src/ngtsc/reflection", + "@npm//@types/node", + "@npm//typescript", + ], +) diff --git a/packages/compiler-cli/src/ngtsc/validation/index.ts b/packages/compiler-cli/src/ngtsc/validation/index.ts new file mode 100644 index 00000000000..a88cf97fa4f --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/validation/index.ts @@ -0,0 +1,9 @@ +/** + * @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 + */ + +export {SourceFileValidator} from './src/source_file_validator'; diff --git a/packages/compiler-cli/src/ngtsc/validation/src/rules/api.ts b/packages/compiler-cli/src/ngtsc/validation/src/rules/api.ts new file mode 100644 index 00000000000..bb95fa7991a --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/validation/src/rules/api.ts @@ -0,0 +1,27 @@ +/*! + * @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 ts from 'typescript'; + +/** + * Rule that checks a source file a specific issue. + */ +export interface SourceFileValidatorRule { + /** + * Whether the file should be checked. Used to stop the traversal of the file early. + * @param sourceFile File to be checked. + */ + shouldCheck(sourceFile: ts.SourceFile): boolean; + + /** + * Produces diagnostics for a specific node that may + * contain the issue that the rule is enforcing. + * @param node Node to be checked. + */ + checkNode(node: ts.Node): ts.Diagnostic[]|null; +} diff --git a/packages/compiler-cli/src/ngtsc/validation/src/source_file_validator.ts b/packages/compiler-cli/src/ngtsc/validation/src/source_file_validator.ts new file mode 100644 index 00000000000..4c90df4f280 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/validation/src/source_file_validator.ts @@ -0,0 +1,62 @@ +/*! + * @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 ts from 'typescript'; + +import {ImportedSymbolsTracker} from '../../imports'; +import {ReflectionHost} from '../../reflection'; + +import {SourceFileValidatorRule} from './rules/api'; + +/** + * Validates that TypeScript files match a specific set of rules set by the Angular compiler. + */ +export class SourceFileValidator { + private rules: SourceFileValidatorRule[]; + + constructor(reflector: ReflectionHost, importedSymbolsTracker: ImportedSymbolsTracker) { + this.rules = []; // TODO: implement the rules. + } + + /** + * Gets the diagnostics for a specific file, or null if the file is valid. + * @param sourceFile File to be checked. + */ + getDiagnosticsForFile(sourceFile: ts.SourceFile): ts.Diagnostic[]|null { + if (sourceFile.isDeclarationFile || sourceFile.fileName.endsWith('.ngtypecheck.ts')) { + return null; + } + + let rulesToRun: SourceFileValidatorRule[]|null = null; + for (const rule of this.rules) { + if (rule.shouldCheck(sourceFile)) { + rulesToRun ??= []; + rulesToRun.push(rule); + } + } + + if (rulesToRun === null) { + return null; + } + + let fileDiagnostics: ts.Diagnostic[]|null = null; + sourceFile.forEachChild(function walk(node) { + // Note: non-null assertion is here because of g3. + for (const rule of rulesToRun!) { + const nodeDiagnostics = rule.checkNode(node); + if (nodeDiagnostics !== null) { + fileDiagnostics ??= []; + fileDiagnostics.push(...nodeDiagnostics); + } + } + node.forEachChild(walk); + }); + + return fileDiagnostics; + } +}