refactor(compiler): Add a new helper method getOwningNgModule. (#47166)

This helper accepts a class for an Angular trait, and returns the NgModule which owns that trait. This will be useful for the language service import project, which needs to edit import arrays on the module.

PR Close #47166
This commit is contained in:
Dylan Hunn
2022-08-18 15:35:08 -07:00
committed by Alex Rickabaugh
parent 7884837a44
commit edf7f670f0
4 changed files with 142 additions and 36 deletions
@@ -151,6 +151,12 @@ export interface TemplateTypeChecker {
*/
getPrimaryAngularDecorator(target: ts.ClassDeclaration): ts.Decorator|null;
/**
* Get the class of the NgModule that owns this Angular trait. If the result is `null`, that
* probably means the provided component is standalone.
*/
getOwningNgModule(component: ts.ClassDeclaration): ts.ClassDeclaration|null;
/**
* Retrieve any potential DOM bindings for the given element.
*
@@ -194,8 +200,8 @@ export interface TemplateTypeChecker {
*/
export enum OptimizeFor {
/**
* Indicates that a consumer of a `TemplateTypeChecker` is only interested in results for a given
* file, and wants them as fast as possible.
* Indicates that a consumer of a `TemplateTypeChecker` is only interested in results for a
* given file, and wants them as fast as possible.
*
* Calling `TemplateTypeChecker` methods successively for multiple files while specifying
* `OptimizeFor.SingleFile` can result in significant unnecessary overhead overall.
@@ -203,8 +209,8 @@ export enum OptimizeFor {
SingleFile,
/**
* Indicates that a consumer of a `TemplateTypeChecker` intends to query for results pertaining to
* the entire user program, and so the type-checker should internally optimize for this case.
* Indicates that a consumer of a `TemplateTypeChecker` intends to query for results pertaining
* to the entire user program, and so the type-checker should internally optimize for this case.
*
* Initial calls to retrieve type-checking information may take longer, but repeated calls to
* gather information for the whole user program will be significantly faster with this mode of
@@ -615,9 +615,6 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
}
getPrimaryAngularDecorator(target: ts.ClassDeclaration): ts.Decorator|null {
// TODO(dylhunn): It seems like we need to call this to make sure that
// `typeCheckAdapter.typeCheck()` is called, which in `ngtsc/typecheck/testing/index.ts:setup`
// actually populates the `metadataRegistry`, which is needed for the `metadataReader` to work.
this.ensureAllShimsForOneFile(target.getSourceFile());
if (!isNamedClassDeclaration(target)) {
@@ -642,6 +639,25 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
return null;
}
getOwningNgModule(component: ts.ClassDeclaration): ts.ClassDeclaration|null {
if (!isNamedClassDeclaration(component)) {
return null;
}
const dirMeta = this.metaReader.getDirectiveMetadata(new Reference(component));
if (dirMeta !== null && dirMeta.isStandalone) {
return null;
}
const scope = this.componentScopeReader.getScopeForComponent(component);
if (scope === null || scope.kind !== ComponentScopeKind.NgModule ||
!isNamedClassDeclaration(scope.ngModule)) {
return null;
}
return scope.ngModule;
}
private getScopeData(component: ts.ClassDeclaration): ScopeData|null {
if (this.scopeCache.has(component)) {
return this.scopeCache.get(component)!;
@@ -10,6 +10,7 @@ import {AST, ASTWithSource, BindingPipe, Call, ParseSourceSpan, PropertyRead, Pr
import ts from 'typescript';
import {AbsoluteFsPath} from '../../file_system';
import {Reference} from '../../imports';
import {ClassDeclaration} from '../../reflection';
import {ComponentScopeKind, ComponentScopeReader} from '../../scope';
import {isAssignment, isSymbolWithValueDeclaration} from '../../util/src/typescript';
@@ -30,19 +30,18 @@ runInEachFileSystem(() => {
env.tsconfig({strictTemplates: true, _enableTemplateTypeChecker: true});
});
describe('supports `getPrimaryAngularDecorator()` ', () => {
it('for components', () => {
env.write('test.ts', `
import {Component} from '@angular/core';
@Component({
standalone: true,
selector: 'test-cmp',
template: '<div></div>',
})
export class TestCmp {}
`);
import {Component} from '@angular/core';
@Component({
standalone: true,
selector: 'test-cmp',
template: '<div></div>',
})
export class TestCmp {}
`);
const {program, checker} = env.driveTemplateTypeChecker();
const sf = program.getSourceFile(_('/test.ts'));
expect(sf).not.toBeNull();
@@ -52,15 +51,15 @@ 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 {
return Math.pow(value, exponent);
}
}
`);
import {Pipe, PipeTransform} from '@angular/core';
@Pipe({name: 'expPipe'})
export class ExpPipe implements PipeTransform {
transform(value: number, exponent = 1): number {
return Math.pow(value, exponent);
}
}
`);
const {program, checker} = env.driveTemplateTypeChecker();
const sf = program.getSourceFile(_('/test.ts'));
expect(sf).not.toBeNull();
@@ -70,16 +69,16 @@ runInEachFileSystem(() => {
it('for NgModules', () => {
env.write('test.ts', `
import {NgModule} from '@angular/core';
@NgModule({
declarations: [],
imports: [],
providers: [],
bootstrap: []
})
export class AppModule {}
`);
import {NgModule} from '@angular/core';
@NgModule({
declarations: [],
imports: [],
providers: [],
bootstrap: []
})
export class AppModule {}
`);
const {program, checker} = env.driveTemplateTypeChecker();
const sf = program.getSourceFile(_('/test.ts'));
expect(sf).not.toBeNull();
@@ -87,5 +86,89 @@ runInEachFileSystem(() => {
expect(decorator?.getText()).toContain(`declarations: []`);
});
});
describe('supports `getOwningNgModule()` ', () => {
it('for components', () => {
env.write('test.ts', `
import {Component, NgModule} from '@angular/core';
@NgModule({
declarations: [AppCmp],
imports: [],
providers: [],
bootstrap: [AppCmp]
})
export class AppModule {}
@Component({
selector: 'app-cmp',
template: '<div></div>',
})
export class AppCmp {}
`);
const {program, checker} = env.driveTemplateTypeChecker();
const sf = program.getSourceFile(_('/test.ts'));
expect(sf).not.toBeNull();
const ngModuleKnownClass = getClass(sf!, 'AppModule');
expect(ngModuleKnownClass).not.toBeNull();
const ngModuleRetrievedClass = checker.getOwningNgModule(getClass(sf!, 'AppCmp'));
expect(ngModuleRetrievedClass).toEqual(ngModuleKnownClass);
});
it('for standalone components (which should be null)', () => {
env.write('test.ts', `
import {Component, NgModule} from '@angular/core';
@NgModule({
declarations: [AppCmp],
imports: [],
providers: [],
bootstrap: [AppCmp]
})
export class AppModule {}
@Component({
selector: 'app-cmp',
template: '<div></div>',
standalone: true,
})
export class AppCmp {}
`);
const {program, checker} = env.driveTemplateTypeChecker();
const sf = program.getSourceFile(_('/test.ts'));
expect(sf).not.toBeNull();
const ngModuleKnownClass = getClass(sf!, 'AppModule');
expect(ngModuleKnownClass).not.toBeNull();
const ngModuleRetrievedClass = checker.getOwningNgModule(getClass(sf!, 'AppCmp'));
expect(ngModuleRetrievedClass).toBe(null);
});
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 {
return Math.pow(value, exponent);
}
}
`);
const {program, checker} = env.driveTemplateTypeChecker();
const sf = program.getSourceFile(_('/test.ts'));
expect(sf).not.toBeNull();
const ngModuleKnownClass = getClass(sf!, 'PipeModule');
expect(ngModuleKnownClass).not.toBeNull();
const ngModuleRetrievedClass = checker.getOwningNgModule(getClass(sf!, 'ExpPipe'));
expect(ngModuleRetrievedClass).toEqual(ngModuleKnownClass);
});
});
});
});