refactor(compiler-cli): introduce infrastructure for running additional checks against TypeScript files (#54993)

Adds the new `SourceFileValidator` that will be used to check for file-level issues that may prevent Angular from working, like invoking the `input()` function outside of an initializer. Currently only one check is planned, but this setup will allow us to easily add more in the future.

PR Close #54993
This commit is contained in:
Kristiyan Kostadinov
2024-03-21 15:17:17 +01:00
committed by Dylan Hunn
parent 3d9f01c6ec
commit 8226be6abf
6 changed files with 132 additions and 1 deletions
@@ -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",
@@ -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,
};
}
}
@@ -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",
],
)
@@ -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';
@@ -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;
}
@@ -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;
}
}