Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions packages/compiler-cli/src/ngtsc/typecheck/api/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -194,17 +200,17 @@ 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.
*/
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
Expand Down
22 changes: 19 additions & 3 deletions packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand All @@ -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)!;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
141 changes: 112 additions & 29 deletions packages/compiler-cli/test/ngtsc/ls_typecheck_helpers_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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();
Expand All @@ -70,22 +69,106 @@ 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();
const decorator = checker.getPrimaryAngularDecorator(getClass(sf!, 'AppModule'));
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);
});
});
});
});