Skip to content

Commit edf7f67

Browse files
dylhunnalxhub
authored andcommitted
refactor(compiler): Add a new helper method getOwningNgModule. (angular#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 angular#47166
1 parent 7884837 commit edf7f67

File tree

4 files changed

+142
-36
lines changed

4 files changed

+142
-36
lines changed

packages/compiler-cli/src/ngtsc/typecheck/api/checker.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,12 @@ export interface TemplateTypeChecker {
151151
*/
152152
getPrimaryAngularDecorator(target: ts.ClassDeclaration): ts.Decorator|null;
153153

154+
/**
155+
* Get the class of the NgModule that owns this Angular trait. If the result is `null`, that
156+
* probably means the provided component is standalone.
157+
*/
158+
getOwningNgModule(component: ts.ClassDeclaration): ts.ClassDeclaration|null;
159+
154160
/**
155161
* Retrieve any potential DOM bindings for the given element.
156162
*
@@ -194,17 +200,17 @@ export interface TemplateTypeChecker {
194200
*/
195201
export enum OptimizeFor {
196202
/**
197-
* Indicates that a consumer of a `TemplateTypeChecker` is only interested in results for a given
198-
* file, and wants them as fast as possible.
203+
* Indicates that a consumer of a `TemplateTypeChecker` is only interested in results for a
204+
* given file, and wants them as fast as possible.
199205
*
200206
* Calling `TemplateTypeChecker` methods successively for multiple files while specifying
201207
* `OptimizeFor.SingleFile` can result in significant unnecessary overhead overall.
202208
*/
203209
SingleFile,
204210

205211
/**
206-
* Indicates that a consumer of a `TemplateTypeChecker` intends to query for results pertaining to
207-
* the entire user program, and so the type-checker should internally optimize for this case.
212+
* Indicates that a consumer of a `TemplateTypeChecker` intends to query for results pertaining
213+
* to the entire user program, and so the type-checker should internally optimize for this case.
208214
*
209215
* Initial calls to retrieve type-checking information may take longer, but repeated calls to
210216
* gather information for the whole user program will be significantly faster with this mode of

packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -615,9 +615,6 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
615615
}
616616

617617
getPrimaryAngularDecorator(target: ts.ClassDeclaration): ts.Decorator|null {
618-
// TODO(dylhunn): It seems like we need to call this to make sure that
619-
// `typeCheckAdapter.typeCheck()` is called, which in `ngtsc/typecheck/testing/index.ts:setup`
620-
// actually populates the `metadataRegistry`, which is needed for the `metadataReader` to work.
621618
this.ensureAllShimsForOneFile(target.getSourceFile());
622619

623620
if (!isNamedClassDeclaration(target)) {
@@ -642,6 +639,25 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
642639
return null;
643640
}
644641

642+
getOwningNgModule(component: ts.ClassDeclaration): ts.ClassDeclaration|null {
643+
if (!isNamedClassDeclaration(component)) {
644+
return null;
645+
}
646+
647+
const dirMeta = this.metaReader.getDirectiveMetadata(new Reference(component));
648+
if (dirMeta !== null && dirMeta.isStandalone) {
649+
return null;
650+
}
651+
652+
const scope = this.componentScopeReader.getScopeForComponent(component);
653+
if (scope === null || scope.kind !== ComponentScopeKind.NgModule ||
654+
!isNamedClassDeclaration(scope.ngModule)) {
655+
return null;
656+
}
657+
658+
return scope.ngModule;
659+
}
660+
645661
private getScopeData(component: ts.ClassDeclaration): ScopeData|null {
646662
if (this.scopeCache.has(component)) {
647663
return this.scopeCache.get(component)!;

packages/compiler-cli/src/ngtsc/typecheck/src/template_symbol_builder.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {AST, ASTWithSource, BindingPipe, Call, ParseSourceSpan, PropertyRead, Pr
1010
import ts from 'typescript';
1111

1212
import {AbsoluteFsPath} from '../../file_system';
13+
import {Reference} from '../../imports';
1314
import {ClassDeclaration} from '../../reflection';
1415
import {ComponentScopeKind, ComponentScopeReader} from '../../scope';
1516
import {isAssignment, isSymbolWithValueDeclaration} from '../../util/src/typescript';

packages/compiler-cli/test/ngtsc/ls_typecheck_helpers_spec.ts

Lines changed: 112 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -30,19 +30,18 @@ runInEachFileSystem(() => {
3030
env.tsconfig({strictTemplates: true, _enableTemplateTypeChecker: true});
3131
});
3232

33-
3433
describe('supports `getPrimaryAngularDecorator()` ', () => {
3534
it('for components', () => {
3635
env.write('test.ts', `
37-
import {Component} from '@angular/core';
38-
39-
@Component({
40-
standalone: true,
41-
selector: 'test-cmp',
42-
template: '<div></div>',
43-
})
44-
export class TestCmp {}
45-
`);
36+
import {Component} from '@angular/core';
37+
38+
@Component({
39+
standalone: true,
40+
selector: 'test-cmp',
41+
template: '<div></div>',
42+
})
43+
export class TestCmp {}
44+
`);
4645
const {program, checker} = env.driveTemplateTypeChecker();
4746
const sf = program.getSourceFile(_('/test.ts'));
4847
expect(sf).not.toBeNull();
@@ -52,15 +51,15 @@ runInEachFileSystem(() => {
5251

5352
it('for pipes', () => {
5453
env.write('test.ts', `
55-
import {Pipe, PipeTransform} from '@angular/core';
56-
57-
@Pipe({name: 'expPipe'})
58-
export class ExpPipe implements PipeTransform {
59-
transform(value: number, exponent = 1): number {
60-
return Math.pow(value, exponent);
61-
}
62-
}
63-
`);
54+
import {Pipe, PipeTransform} from '@angular/core';
55+
56+
@Pipe({name: 'expPipe'})
57+
export class ExpPipe implements PipeTransform {
58+
transform(value: number, exponent = 1): number {
59+
return Math.pow(value, exponent);
60+
}
61+
}
62+
`);
6463
const {program, checker} = env.driveTemplateTypeChecker();
6564
const sf = program.getSourceFile(_('/test.ts'));
6665
expect(sf).not.toBeNull();
@@ -70,22 +69,106 @@ runInEachFileSystem(() => {
7069

7170
it('for NgModules', () => {
7271
env.write('test.ts', `
73-
import {NgModule} from '@angular/core';
74-
75-
@NgModule({
76-
declarations: [],
77-
imports: [],
78-
providers: [],
79-
bootstrap: []
80-
})
81-
export class AppModule {}
82-
`);
72+
import {NgModule} from '@angular/core';
73+
74+
@NgModule({
75+
declarations: [],
76+
imports: [],
77+
providers: [],
78+
bootstrap: []
79+
})
80+
export class AppModule {}
81+
`);
8382
const {program, checker} = env.driveTemplateTypeChecker();
8483
const sf = program.getSourceFile(_('/test.ts'));
8584
expect(sf).not.toBeNull();
8685
const decorator = checker.getPrimaryAngularDecorator(getClass(sf!, 'AppModule'));
8786
expect(decorator?.getText()).toContain(`declarations: []`);
8887
});
8988
});
89+
90+
describe('supports `getOwningNgModule()` ', () => {
91+
it('for components', () => {
92+
env.write('test.ts', `
93+
import {Component, NgModule} from '@angular/core';
94+
95+
@NgModule({
96+
declarations: [AppCmp],
97+
imports: [],
98+
providers: [],
99+
bootstrap: [AppCmp]
100+
})
101+
export class AppModule {}
102+
103+
@Component({
104+
selector: 'app-cmp',
105+
template: '<div></div>',
106+
})
107+
export class AppCmp {}
108+
`);
109+
const {program, checker} = env.driveTemplateTypeChecker();
110+
const sf = program.getSourceFile(_('/test.ts'));
111+
expect(sf).not.toBeNull();
112+
const ngModuleKnownClass = getClass(sf!, 'AppModule');
113+
expect(ngModuleKnownClass).not.toBeNull();
114+
const ngModuleRetrievedClass = checker.getOwningNgModule(getClass(sf!, 'AppCmp'));
115+
expect(ngModuleRetrievedClass).toEqual(ngModuleKnownClass);
116+
});
117+
118+
it('for standalone components (which should be null)', () => {
119+
env.write('test.ts', `
120+
import {Component, NgModule} from '@angular/core';
121+
122+
@NgModule({
123+
declarations: [AppCmp],
124+
imports: [],
125+
providers: [],
126+
bootstrap: [AppCmp]
127+
})
128+
export class AppModule {}
129+
130+
@Component({
131+
selector: 'app-cmp',
132+
template: '<div></div>',
133+
standalone: true,
134+
})
135+
export class AppCmp {}
136+
`);
137+
const {program, checker} = env.driveTemplateTypeChecker();
138+
const sf = program.getSourceFile(_('/test.ts'));
139+
expect(sf).not.toBeNull();
140+
const ngModuleKnownClass = getClass(sf!, 'AppModule');
141+
expect(ngModuleKnownClass).not.toBeNull();
142+
const ngModuleRetrievedClass = checker.getOwningNgModule(getClass(sf!, 'AppCmp'));
143+
expect(ngModuleRetrievedClass).toBe(null);
144+
});
145+
146+
it('for pipes', () => {
147+
env.write('test.ts', `
148+
import {Component, NgModule, Pipe, PipeTransform} from '@angular/core';
149+
150+
@NgModule({
151+
declarations: [ExpPipe],
152+
imports: [],
153+
providers: [],
154+
})
155+
export class PipeModule {}
156+
157+
@Pipe({name: 'expPipe'})
158+
export class ExpPipe implements PipeTransform {
159+
transform(value: number, exponent = 1): number {
160+
return Math.pow(value, exponent);
161+
}
162+
}
163+
`);
164+
const {program, checker} = env.driveTemplateTypeChecker();
165+
const sf = program.getSourceFile(_('/test.ts'));
166+
expect(sf).not.toBeNull();
167+
const ngModuleKnownClass = getClass(sf!, 'PipeModule');
168+
expect(ngModuleKnownClass).not.toBeNull();
169+
const ngModuleRetrievedClass = checker.getOwningNgModule(getClass(sf!, 'ExpPipe'));
170+
expect(ngModuleRetrievedClass).toEqual(ngModuleKnownClass);
171+
});
172+
});
90173
});
91174
});

0 commit comments

Comments
 (0)