diff --git a/packages/compiler-cli/private/BUILD.bazel b/packages/compiler-cli/private/BUILD.bazel index c95c1328d69..0c6ab20e970 100644 --- a/packages/compiler-cli/private/BUILD.bazel +++ b/packages/compiler-cli/private/BUILD.bazel @@ -14,6 +14,7 @@ ts_library( "//packages/compiler-cli/src/ngtsc/perf", "//packages/compiler-cli/src/ngtsc/reflection", "//packages/compiler-cli/src/ngtsc/sourcemaps", + "//packages/compiler-cli/src/ngtsc/typecheck/api", "//packages/compiler-cli/src/transformers/downlevel_decorators_transform", "@npm//typescript", ], diff --git a/packages/compiler-cli/private/migrations.ts b/packages/compiler-cli/private/migrations.ts index bb97ed78e60..02147dc40d4 100644 --- a/packages/compiler-cli/private/migrations.ts +++ b/packages/compiler-cli/private/migrations.ts @@ -15,3 +15,4 @@ export {forwardRefResolver} from '../src/ngtsc/annotations'; export {Reference} from '../src/ngtsc/imports'; export {DynamicValue, PartialEvaluator, ResolvedValue, ResolvedValueMap, StaticInterpreter} from '../src/ngtsc/partial_evaluator'; export {reflectObjectLiteral, TypeScriptReflectionHost} from '../src/ngtsc/reflection'; +export {PotentialImport, PotentialImportKind, PotentialImportMode, TemplateTypeChecker} from '../src/ngtsc/typecheck/api'; diff --git a/packages/compiler-cli/src/ngtsc/typecheck/api/api.ts b/packages/compiler-cli/src/ngtsc/typecheck/api/api.ts index e9912c4a215..28b3959f148 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/api/api.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/api/api.ts @@ -26,6 +26,7 @@ export interface TypeCheckableDirectiveMeta extends DirectiveMeta, DirectiveType outputs: ClassPropertyMapping; isStandalone: boolean; hostDirectives: HostDirectiveMeta[]|null; + decorator: ts.Decorator|null; } export type TemplateId = string&{__brand: 'TemplateId'}; diff --git a/packages/compiler-cli/src/ngtsc/typecheck/api/checker.ts b/packages/compiler-cli/src/ngtsc/typecheck/api/checker.ts index ad3498204c0..10d0f60c682 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/api/checker.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/api/checker.ts @@ -11,10 +11,13 @@ import ts from 'typescript'; import {AbsoluteFsPath} from '../../../../src/ngtsc/file_system'; import {ErrorCode} from '../../diagnostics'; +import {Reference} from '../../imports'; +import {NgModuleMeta, PipeMeta} from '../../metadata'; +import {ClassDeclaration} from '../../reflection'; import {FullTemplateMapping, NgTemplateDiagnostic, TypeCheckableDirectiveMeta} from './api'; import {GlobalCompletion} from './completion'; -import {PotentialDirective, PotentialImport, PotentialPipe} from './scope'; +import {PotentialDirective, PotentialImport, PotentialImportMode, PotentialPipe} from './scope'; import {ElementSymbol, Symbol, TcbLocation, TemplateSymbol} from './symbols'; /** @@ -150,8 +153,8 @@ export interface TemplateTypeChecker { * In the context of an Angular trait, generate potential imports for a directive. */ getPotentialImportsFor( - toImport: PotentialDirective|PotentialPipe, - inComponent: ts.ClassDeclaration): ReadonlyArray; + toImport: Reference, inComponent: ts.ClassDeclaration, + importMode: PotentialImportMode): ReadonlyArray; /** * Get the primary decorator for an Angular class (such as @Component). This does not work for @@ -184,6 +187,26 @@ export interface TemplateTypeChecker { */ getDirectiveMetadata(dir: ts.ClassDeclaration): TypeCheckableDirectiveMeta|null; + /** + * Retrieve the type checking engine's metadata for the given NgModule class, if available. + */ + getNgModuleMetadata(module: ts.ClassDeclaration): NgModuleMeta|null; + + /** + * Retrieve the type checking engine's metadata for the given pipe class, if available. + */ + getPipeMetadata(pipe: ts.ClassDeclaration): PipeMeta|null; + + /** + * Gets the directives that have been used in a component's template. + */ + getUsedDirectives(component: ts.ClassDeclaration): TypeCheckableDirectiveMeta[]|null; + + /** + * Gets the pipes that have been used in a component's template. + */ + getUsedPipes(component: ts.ClassDeclaration): string[]|null; + /** * Reset the `TemplateTypeChecker`'s state for the given class, so that it will be recomputed on * the next request. diff --git a/packages/compiler-cli/src/ngtsc/typecheck/api/scope.ts b/packages/compiler-cli/src/ngtsc/typecheck/api/scope.ts index cb0f26dc781..59d57946e6c 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/api/scope.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/api/scope.ts @@ -89,3 +89,18 @@ export interface PotentialPipe { */ isInScope: boolean; } + +/** + * Possible modes in which to look up a potential import. + */ +export enum PotentialImportMode { + /** Whether an import is standalone is inferred based on its metadata. */ + Normal, + + /** + * An import is assumed to be standalone and is imported directly. This is useful for migrations + * where a declaration wasn't standalone when the program was created, but will become standalone + * as a part of the migration. + */ + ForceDirect, +} diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts index 9e3de67a1c1..83ecb308717 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts @@ -6,21 +6,21 @@ * found in the LICENSE file at https://angular.io/license */ -import {AST, CssSelector, DomElementSchemaRegistry, ExternalExpr, LiteralPrimitive, ParseSourceSpan, PropertyRead, SafePropertyRead, TmplAstElement, TmplAstNode, TmplAstReference, TmplAstTemplate, TmplAstTextAttribute, WrappedNodeExpr} from '@angular/compiler'; +import {AST, CssSelector, DomElementSchemaRegistry, ExternalExpr, LiteralPrimitive, ParseSourceSpan, PropertyRead, SafePropertyRead, TmplAstElement, TmplAstNode, TmplAstTemplate, TmplAstTextAttribute, WrappedNodeExpr} from '@angular/compiler'; import ts from 'typescript'; import {ErrorCode, ngErrorCode} from '../../diagnostics'; -import {absoluteFrom, absoluteFromSourceFile, AbsoluteFsPath, getSourceFileOrError} from '../../file_system'; +import {absoluteFromSourceFile, AbsoluteFsPath, getSourceFileOrError} from '../../file_system'; import {Reference, ReferenceEmitKind, ReferenceEmitter} from '../../imports'; import {IncrementalBuild} from '../../incremental/api'; -import {DirectiveMeta, MetadataReader, MetadataReaderWithIndex, MetaKind, NgModuleIndex, PipeMeta} from '../../metadata'; +import {DirectiveMeta, MetadataReader, MetadataReaderWithIndex, MetaKind, NgModuleIndex, NgModuleMeta, PipeMeta} from '../../metadata'; import {PerfCheckpoint, PerfEvent, PerfPhase, PerfRecorder} from '../../perf'; import {ProgramDriver, UpdateMode} from '../../program_driver'; import {ClassDeclaration, DeclarationNode, isNamedClassDeclaration, ReflectionHost} from '../../reflection'; import {ComponentScopeKind, ComponentScopeReader, TypeCheckScopeRegistry} from '../../scope'; import {isShim} from '../../shims'; import {getSourceFileOrNull, isSymbolWithValueDeclaration} from '../../util/src/typescript'; -import {ElementSymbol, FullTemplateMapping, GlobalCompletion, NgTemplateDiagnostic, OptimizeFor, PotentialDirective, PotentialImport, PotentialImportKind, PotentialPipe, ProgramTypeCheckAdapter, Symbol, TcbLocation, TemplateDiagnostic, TemplateId, TemplateSymbol, TemplateTypeChecker, TypeCheckableDirectiveMeta, TypeCheckingConfig} from '../api'; +import {ElementSymbol, FullTemplateMapping, GlobalCompletion, NgTemplateDiagnostic, OptimizeFor, PotentialDirective, PotentialImport, PotentialImportKind, PotentialImportMode, PotentialPipe, ProgramTypeCheckAdapter, Symbol, TcbLocation, TemplateDiagnostic, TemplateId, TemplateSymbol, TemplateTypeChecker, TypeCheckableDirectiveMeta, TypeCheckingConfig} from '../api'; import {makeTemplateDiagnostic} from '../diagnostics'; import {CompletionEngine} from './completion'; @@ -99,6 +99,14 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { return data.template; } + getUsedDirectives(component: ts.ClassDeclaration): TypeCheckableDirectiveMeta[]|null { + return this.getLatestComponentState(component).data?.boundTarget.getUsedDirectives() || null; + } + + getUsedPipes(component: ts.ClassDeclaration): string[]|null { + return this.getLatestComponentState(component).data?.boundTarget.getUsedPipes() || null; + } + private getLatestComponentState(component: ts.ClassDeclaration): {data: TemplateData|null, tcb: ts.Node|null, tcbPath: AbsoluteFsPath, tcbIsShim: boolean} { this.ensureShimForComponent(component); @@ -598,6 +606,20 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { return this.typeCheckScopeRegistry.getTypeCheckDirectiveMetadata(new Reference(dir)); } + getNgModuleMetadata(module: ts.ClassDeclaration): NgModuleMeta|null { + if (!isNamedClassDeclaration(module)) { + return null; + } + return this.metaReader.getNgModuleMetadata(new Reference(module)); + } + + getPipeMetadata(pipe: ts.ClassDeclaration): PipeMeta|null { + if (!isNamedClassDeclaration(pipe)) { + return null; + } + return this.metaReader.getPipeMetadata(new Reference(pipe)); + } + getPotentialElementTags(component: ts.ClassDeclaration): Map { if (this.elementTagCache.has(component)) { return this.elementTagCache.get(component)!; @@ -712,18 +734,18 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { } getPotentialImportsFor( - toImport: PotentialDirective|PotentialPipe, - inContext: ts.ClassDeclaration): ReadonlyArray { + toImport: Reference, inContext: ts.ClassDeclaration, + importMode: PotentialImportMode): ReadonlyArray { const imports: PotentialImport[] = []; - const meta = this.metaReader.getDirectiveMetadata(toImport.ref) ?? - this.metaReader.getPipeMetadata(toImport.ref); + const meta = + this.metaReader.getDirectiveMetadata(toImport) ?? this.metaReader.getPipeMetadata(toImport); if (meta === null) { return imports; } - if (meta.isStandalone) { - const emitted = this.emit(PotentialImportKind.Standalone, toImport.ref, inContext); + if (meta.isStandalone || importMode === PotentialImportMode.ForceDirect) { + const emitted = this.emit(PotentialImportKind.Standalone, toImport, inContext); if (emitted !== null) { imports.push(emitted); } @@ -732,7 +754,7 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { const exportingNgModules = this.ngModuleIndex.getNgModulesExporting(meta.ref.node); if (exportingNgModules !== null) { for (const exporter of exportingNgModules) { - const emittedRef = this.emit(PotentialImportKind.Standalone, exporter, inContext); + const emittedRef = this.emit(PotentialImportKind.NgModule, exporter, inContext); if (emittedRef !== null) { imports.push(emittedRef); } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/testing/index.ts b/packages/compiler-cli/src/ngtsc/typecheck/testing/index.ts index 65abc20e405..6163eeac6e1 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/testing/index.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/testing/index.ts @@ -663,6 +663,7 @@ function getDirectiveMetaFromDeclaration( isStandalone: !!decl.isStandalone, baseClass: null, animationTriggerNames: null, + decorator: null, hostDirectives: decl.hostDirectives === undefined ? null : decl.hostDirectives.map(hostDecl => { return { directive: new Reference(resolveDeclaration(hostDecl.directive)), diff --git a/packages/compiler-cli/test/ngtsc/ls_typecheck_helpers_spec.ts b/packages/compiler-cli/test/ngtsc/ls_typecheck_helpers_spec.ts index 1cc67610ca4..d658d864306 100644 --- a/packages/compiler-cli/test/ngtsc/ls_typecheck_helpers_spec.ts +++ b/packages/compiler-cli/test/ngtsc/ls_typecheck_helpers_spec.ts @@ -5,6 +5,7 @@ * 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 {PotentialImportMode} from '@angular/compiler-cli/src/ngtsc/typecheck/api'; import ts from 'typescript'; import {DiagnosticCategoryLabel} from '../../src/ngtsc/core/api'; @@ -34,7 +35,7 @@ runInEachFileSystem(() => { it('for components', () => { env.write('test.ts', ` import {Component} from '@angular/core'; - + @Component({ standalone: true, selector: 'test-cmp', @@ -52,7 +53,7 @@ runInEachFileSystem(() => { it('for pipes', () => { env.write('test.ts', ` import {Pipe, PipeTransform} from '@angular/core'; - + @Pipe({name: 'expPipe'}) export class ExpPipe implements PipeTransform { transform(value: number, exponent = 1): number { @@ -70,7 +71,7 @@ runInEachFileSystem(() => { it('for NgModules', () => { env.write('test.ts', ` import {NgModule} from '@angular/core'; - + @NgModule({ declarations: [], imports: [], @@ -91,7 +92,7 @@ runInEachFileSystem(() => { it('for components', () => { env.write('test.ts', ` import {Component, NgModule} from '@angular/core'; - + @NgModule({ declarations: [AppCmp], imports: [], @@ -99,7 +100,7 @@ runInEachFileSystem(() => { bootstrap: [AppCmp] }) export class AppModule {} - + @Component({ selector: 'app-cmp', template: '
', @@ -118,7 +119,7 @@ runInEachFileSystem(() => { it('for standalone components (which should be null)', () => { env.write('test.ts', ` import {Component, NgModule} from '@angular/core'; - + @NgModule({ declarations: [AppCmp], imports: [], @@ -126,7 +127,7 @@ runInEachFileSystem(() => { bootstrap: [AppCmp] }) export class AppModule {} - + @Component({ selector: 'app-cmp', template: '
', @@ -146,14 +147,14 @@ runInEachFileSystem(() => { it('for pipes', () => { env.write('test.ts', ` import {Component, NgModule, Pipe, PipeTransform} from '@angular/core'; - + @NgModule({ declarations: [ExpPipe], imports: [], providers: [], }) export class PipeModule {} - + @Pipe({name: 'expPipe'}) export class ExpPipe implements PipeTransform { transform(value: number, exponent = 1): number { @@ -175,7 +176,7 @@ runInEachFileSystem(() => { it('which are out of scope', () => { env.write('one.ts', ` import {Component} from '@angular/core'; - + @Component({ standalone: true, selector: 'one-cmp', @@ -186,7 +187,7 @@ runInEachFileSystem(() => { env.write('two.ts', ` import {Component} from '@angular/core'; - + @Component({ standalone: true, selector: 'two-cmp', @@ -206,7 +207,7 @@ runInEachFileSystem(() => { it('which are out of scope', () => { env.write('one.ts', ` import {Pipe} from '@angular/core'; - + @Pipe({ name: 'foo-pipe', standalone: true, @@ -217,7 +218,7 @@ runInEachFileSystem(() => { env.write('two.ts', ` import {Component} from '@angular/core'; - + @Component({ standalone: true, selector: 'two-cmp', @@ -237,7 +238,7 @@ runInEachFileSystem(() => { it('for out of scope standalone components', () => { env.write('one.ts', ` import {Component} from '@angular/core'; - + @Component({ standalone: true, selector: 'one-cmp', @@ -248,7 +249,7 @@ runInEachFileSystem(() => { env.write('two.ts', ` import {Component} from '@angular/core'; - + @Component({ standalone: true, selector: 'two-cmp', @@ -263,7 +264,8 @@ runInEachFileSystem(() => { const TwoCmpDir = checker.getPotentialTemplateDirectives(OneCmpClass) .filter(d => d.selector === 'two-cmp')[0]; - const imports = checker.getPotentialImportsFor(TwoCmpDir, OneCmpClass); + const imports = + checker.getPotentialImportsFor(TwoCmpDir.ref, OneCmpClass, PotentialImportMode.Normal); expect(imports.length).toBe(1); expect(imports[0].moduleSpecifier).toBe('./two'); @@ -273,7 +275,7 @@ runInEachFileSystem(() => { it('for out of scope ngModules', () => { env.write('one.ts', ` import {Component} from '@angular/core'; - + @Component({ standalone: true, selector: 'one-cmp', @@ -284,7 +286,7 @@ runInEachFileSystem(() => { env.write('two.ts', ` import {Component} from '@angular/core'; - + @Component({ selector: 'two-cmp', template: '
', @@ -296,7 +298,7 @@ runInEachFileSystem(() => { import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { TwoCmp } from './two'; - + @NgModule({ declarations: [ TwoCmp @@ -318,7 +320,8 @@ runInEachFileSystem(() => { const TwoNgMod = checker.getPotentialTemplateDirectives(OneCmpClass) .filter(d => d.selector === 'two-cmp')[0]; - const imports = checker.getPotentialImportsFor(TwoNgMod, OneCmpClass); + const imports = + checker.getPotentialImportsFor(TwoNgMod.ref, OneCmpClass, PotentialImportMode.Normal); expect(imports.length).toBe(1); expect(imports[0].moduleSpecifier).toBe('./twomod'); diff --git a/packages/compiler/src/render3/view/t2_binder.ts b/packages/compiler/src/render3/view/t2_binder.ts index c4c8db329c3..be234e5901d 100644 --- a/packages/compiler/src/render3/view/t2_binder.ts +++ b/packages/compiler/src/render3/view/t2_binder.ts @@ -331,8 +331,6 @@ class DirectiveBinder implements Visitor { class TemplateBinder extends RecursiveAstVisitor implements Visitor { private visitNode: (node: Node) => void; - private pipesUsed: string[] = []; - private constructor( private bindings: Map, private symbols: Map, private usedPipes: Set, diff --git a/packages/language-service/src/codefixes/fix_missing_import.ts b/packages/language-service/src/codefixes/fix_missing_import.ts index 508c50545ae..8edb2fc0ff8 100644 --- a/packages/language-service/src/codefixes/fix_missing_import.ts +++ b/packages/language-service/src/codefixes/fix_missing_import.ts @@ -8,7 +8,7 @@ import {ASTWithName} from '@angular/compiler'; import {ErrorCode as NgCompilerErrorCode, ngErrorCode} from '@angular/compiler-cli/src/ngtsc/diagnostics/index'; -import {PotentialDirective, PotentialPipe} from '@angular/compiler-cli/src/ngtsc/typecheck/api'; +import {PotentialDirective, PotentialImportMode, PotentialPipe} from '@angular/compiler-cli/src/ngtsc/typecheck/api'; import * as t from '@angular/compiler/src/render3/r3_ast'; // t for template AST import ts from 'typescript'; @@ -72,7 +72,8 @@ function getCodeActions( } for (const currMatch of matches.values()) { const currMatchSymbol = currMatch.tsSymbol.valueDeclaration!; - const potentialImports = checker.getPotentialImportsFor(currMatch, importOn); + const potentialImports = + checker.getPotentialImportsFor(currMatch.ref, importOn, PotentialImportMode.Normal); for (let potentialImport of potentialImports) { let [fileImportChanges, importName] = updateImportsForTypescriptFile( tsChecker, importOn.getSourceFile(), potentialImport, currMatchSymbol.getSourceFile());