refactor(compiler): Add ngModule to directive symbol (#39099)
This is needed so that the Language Service can provide the module name in the quick info for a directive/component. To accomplish this, the compiler's `LocalModuleScope` is provided to the `TemplateTypeCheckerImpl`. This will also allow the `TemplateTypeChecker` to provide more completions in the future, giving it a way to determine all the directives/pipes/etc. available to a template. PR Close #39099
This commit is contained in:
parent
57f8dd2978
commit
e10b3e22ac
@ -21,7 +21,7 @@ import {CompoundMetadataReader, CompoundMetadataRegistry, DtsMetadataReader, Inj
|
|||||||
import {ModuleWithProvidersScanner} from '../../modulewithproviders';
|
import {ModuleWithProvidersScanner} from '../../modulewithproviders';
|
||||||
import {PartialEvaluator} from '../../partial_evaluator';
|
import {PartialEvaluator} from '../../partial_evaluator';
|
||||||
import {NOOP_PERF_RECORDER, PerfRecorder} from '../../perf';
|
import {NOOP_PERF_RECORDER, PerfRecorder} from '../../perf';
|
||||||
import {TypeScriptReflectionHost} from '../../reflection';
|
import {ClassDeclaration, TypeScriptReflectionHost} from '../../reflection';
|
||||||
import {AdapterResourceLoader} from '../../resource';
|
import {AdapterResourceLoader} from '../../resource';
|
||||||
import {entryPointKeyFor, NgModuleRouteAnalyzer} from '../../routing';
|
import {entryPointKeyFor, NgModuleRouteAnalyzer} from '../../routing';
|
||||||
import {ComponentScopeReader, LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../scope';
|
import {ComponentScopeReader, LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../scope';
|
||||||
@ -780,7 +780,8 @@ export class NgCompiler {
|
|||||||
|
|
||||||
const templateTypeChecker = new TemplateTypeCheckerImpl(
|
const templateTypeChecker = new TemplateTypeCheckerImpl(
|
||||||
this.tsProgram, this.typeCheckingProgramStrategy, traitCompiler,
|
this.tsProgram, this.typeCheckingProgramStrategy, traitCompiler,
|
||||||
this.getTypeCheckingConfig(), refEmitter, reflector, this.adapter, this.incrementalDriver);
|
this.getTypeCheckingConfig(), refEmitter, reflector, this.adapter, this.incrementalDriver,
|
||||||
|
scopeRegistry);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isCore,
|
isCore,
|
||||||
|
@ -16,6 +16,7 @@ ts_library(
|
|||||||
"//packages/compiler-cli/src/ngtsc/incremental:api",
|
"//packages/compiler-cli/src/ngtsc/incremental:api",
|
||||||
"//packages/compiler-cli/src/ngtsc/metadata",
|
"//packages/compiler-cli/src/ngtsc/metadata",
|
||||||
"//packages/compiler-cli/src/ngtsc/reflection",
|
"//packages/compiler-cli/src/ngtsc/reflection",
|
||||||
|
"//packages/compiler-cli/src/ngtsc/scope",
|
||||||
"//packages/compiler-cli/src/ngtsc/shims",
|
"//packages/compiler-cli/src/ngtsc/shims",
|
||||||
"//packages/compiler-cli/src/ngtsc/shims:api",
|
"//packages/compiler-cli/src/ngtsc/shims:api",
|
||||||
"//packages/compiler-cli/src/ngtsc/translator",
|
"//packages/compiler-cli/src/ngtsc/translator",
|
||||||
|
@ -10,6 +10,7 @@ import {TmplAstElement, TmplAstReference, TmplAstTemplate, TmplAstVariable} from
|
|||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
import {AbsoluteFsPath} from '../../file_system';
|
import {AbsoluteFsPath} from '../../file_system';
|
||||||
|
import {ClassDeclaration} from '../../reflection';
|
||||||
|
|
||||||
export enum SymbolKind {
|
export enum SymbolKind {
|
||||||
Input,
|
Input,
|
||||||
@ -238,6 +239,9 @@ export interface DirectiveSymbol {
|
|||||||
|
|
||||||
/** `true` if this `DirectiveSymbol` is for a @Component. */
|
/** `true` if this `DirectiveSymbol` is for a @Component. */
|
||||||
isComponent: boolean;
|
isComponent: boolean;
|
||||||
|
|
||||||
|
/** The `NgModule` that this directive is declared in or `null` if it could not be determined. */
|
||||||
|
ngModule: ClassDeclaration|null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -13,6 +13,7 @@ import {absoluteFromSourceFile, AbsoluteFsPath, getSourceFileOrError} from '../.
|
|||||||
import {ReferenceEmitter} from '../../imports';
|
import {ReferenceEmitter} from '../../imports';
|
||||||
import {IncrementalBuild} from '../../incremental/api';
|
import {IncrementalBuild} from '../../incremental/api';
|
||||||
import {ReflectionHost} from '../../reflection';
|
import {ReflectionHost} from '../../reflection';
|
||||||
|
import {ComponentScopeReader} from '../../scope';
|
||||||
import {isShim} from '../../shims';
|
import {isShim} from '../../shims';
|
||||||
import {getSourceFileOrNull} from '../../util/src/typescript';
|
import {getSourceFileOrNull} from '../../util/src/typescript';
|
||||||
import {OptimizeFor, ProgramTypeCheckAdapter, Symbol, TemplateId, TemplateTypeChecker, TypeCheckingConfig, TypeCheckingProgramStrategy, UpdateMode} from '../api';
|
import {OptimizeFor, ProgramTypeCheckAdapter, Symbol, TemplateId, TemplateTypeChecker, TypeCheckingConfig, TypeCheckingProgramStrategy, UpdateMode} from '../api';
|
||||||
@ -38,7 +39,8 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
|
|||||||
private typeCheckAdapter: ProgramTypeCheckAdapter, private config: TypeCheckingConfig,
|
private typeCheckAdapter: ProgramTypeCheckAdapter, private config: TypeCheckingConfig,
|
||||||
private refEmitter: ReferenceEmitter, private reflector: ReflectionHost,
|
private refEmitter: ReferenceEmitter, private reflector: ReflectionHost,
|
||||||
private compilerHost: Pick<ts.CompilerHost, 'getCanonicalFileName'>,
|
private compilerHost: Pick<ts.CompilerHost, 'getCanonicalFileName'>,
|
||||||
private priorBuild: IncrementalBuild<unknown, FileTypeCheckingData>) {}
|
private priorBuild: IncrementalBuild<unknown, FileTypeCheckingData>,
|
||||||
|
private readonly componentScopeReader: ComponentScopeReader) {}
|
||||||
|
|
||||||
resetOverrides(): void {
|
resetOverrides(): void {
|
||||||
for (const fileRecord of this.state.values()) {
|
for (const fileRecord of this.state.values()) {
|
||||||
@ -372,7 +374,8 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new SymbolBuilder(typeChecker, shimPath, tcb, data).getSymbol(node);
|
return new SymbolBuilder(typeChecker, shimPath, tcb, data, this.componentScopeReader)
|
||||||
|
.getSymbol(node);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,6 +10,8 @@ import {AST, ASTWithSource, BindingPipe, MethodCall, PropertyWrite, SafeMethodCa
|
|||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
import {AbsoluteFsPath} from '../../file_system';
|
import {AbsoluteFsPath} from '../../file_system';
|
||||||
|
import {ClassDeclaration} from '../../reflection';
|
||||||
|
import {ComponentScopeReader} from '../../scope';
|
||||||
import {isAssignment} from '../../util/src/typescript';
|
import {isAssignment} from '../../util/src/typescript';
|
||||||
import {DirectiveSymbol, DomBindingSymbol, ElementSymbol, ExpressionSymbol, InputBindingSymbol, OutputBindingSymbol, ReferenceSymbol, Symbol, SymbolKind, TemplateSymbol, TsNodeSymbolInfo, TypeCheckableDirectiveMeta, VariableSymbol} from '../api';
|
import {DirectiveSymbol, DomBindingSymbol, ElementSymbol, ExpressionSymbol, InputBindingSymbol, OutputBindingSymbol, ReferenceSymbol, Symbol, SymbolKind, TemplateSymbol, TsNodeSymbolInfo, TypeCheckableDirectiveMeta, VariableSymbol} from '../api';
|
||||||
|
|
||||||
@ -24,8 +26,12 @@ import {TcbDirectiveOutputsOp} from './type_check_block';
|
|||||||
*/
|
*/
|
||||||
export class SymbolBuilder {
|
export class SymbolBuilder {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly typeChecker: ts.TypeChecker, private readonly shimPath: AbsoluteFsPath,
|
private readonly typeChecker: ts.TypeChecker,
|
||||||
private readonly typeCheckBlock: ts.Node, private readonly templateData: TemplateData) {}
|
private readonly shimPath: AbsoluteFsPath,
|
||||||
|
private readonly typeCheckBlock: ts.Node,
|
||||||
|
private readonly templateData: TemplateData,
|
||||||
|
private readonly componentScopeReader: ComponentScopeReader,
|
||||||
|
) {}
|
||||||
|
|
||||||
getSymbol(node: TmplAstTemplate|TmplAstElement): TemplateSymbol|ElementSymbol|null;
|
getSymbol(node: TmplAstTemplate|TmplAstElement): TemplateSymbol|ElementSymbol|null;
|
||||||
getSymbol(node: TmplAstReference|TmplAstVariable): ReferenceSymbol|VariableSymbol|null;
|
getSymbol(node: TmplAstReference|TmplAstVariable): ReferenceSymbol|VariableSymbol|null;
|
||||||
@ -99,14 +105,16 @@ export class SymbolBuilder {
|
|||||||
.map(node => {
|
.map(node => {
|
||||||
const symbol = this.getSymbolOfTsNode(node.parent);
|
const symbol = this.getSymbolOfTsNode(node.parent);
|
||||||
if (symbol === null || symbol.tsSymbol === null ||
|
if (symbol === null || symbol.tsSymbol === null ||
|
||||||
symbol.tsSymbol.declarations.length === 0) {
|
symbol.tsSymbol.valueDeclaration === undefined ||
|
||||||
|
!ts.isClassDeclaration(symbol.tsSymbol.valueDeclaration)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const meta = this.getDirectiveMeta(element, symbol.tsSymbol.declarations[0]);
|
const meta = this.getDirectiveMeta(element, symbol.tsSymbol.valueDeclaration);
|
||||||
if (meta === null) {
|
if (meta === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ngModule = this.getDirectiveModule(symbol.tsSymbol.valueDeclaration);
|
||||||
const selector = meta.selector ?? null;
|
const selector = meta.selector ?? null;
|
||||||
const isComponent = meta.isComponent ?? null;
|
const isComponent = meta.isComponent ?? null;
|
||||||
const directiveSymbol: DirectiveSymbol = {
|
const directiveSymbol: DirectiveSymbol = {
|
||||||
@ -114,6 +122,7 @@ export class SymbolBuilder {
|
|||||||
tsSymbol: symbol.tsSymbol,
|
tsSymbol: symbol.tsSymbol,
|
||||||
selector,
|
selector,
|
||||||
isComponent,
|
isComponent,
|
||||||
|
ngModule,
|
||||||
kind: SymbolKind.Directive
|
kind: SymbolKind.Directive
|
||||||
};
|
};
|
||||||
return directiveSymbol;
|
return directiveSymbol;
|
||||||
@ -132,6 +141,14 @@ export class SymbolBuilder {
|
|||||||
return directives.find(m => m.ref.node === directiveDeclaration) ?? null;
|
return directives.find(m => m.ref.node === directiveDeclaration) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getDirectiveModule(declaration: ts.ClassDeclaration): ClassDeclaration|null {
|
||||||
|
const scope = this.componentScopeReader.getScopeForComponent(declaration as ClassDeclaration);
|
||||||
|
if (scope === null || scope === 'error') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return scope.ngModule;
|
||||||
|
}
|
||||||
|
|
||||||
private getSymbolOfBoundEvent(eventBinding: TmplAstBoundEvent): OutputBindingSymbol|null {
|
private getSymbolOfBoundEvent(eventBinding: TmplAstBoundEvent): OutputBindingSymbol|null {
|
||||||
// Outputs are a `ts.CallExpression` that look like one of the two:
|
// Outputs are a `ts.CallExpression` that look like one of the two:
|
||||||
// * _outputHelper(_t1["outputField"]).subscribe(handler);
|
// * _outputHelper(_t1["outputField"]).subscribe(handler);
|
||||||
@ -239,10 +256,13 @@ export class SymbolBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const symbol = this.getSymbolOfTsNode(declaration);
|
const symbol = this.getSymbolOfTsNode(declaration);
|
||||||
if (symbol === null || symbol.tsSymbol === null) {
|
if (symbol === null || symbol.tsSymbol === null ||
|
||||||
|
symbol.tsSymbol.valueDeclaration === undefined ||
|
||||||
|
!ts.isClassDeclaration(symbol.tsSymbol.valueDeclaration)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ngModule = this.getDirectiveModule(symbol.tsSymbol.valueDeclaration);
|
||||||
return {
|
return {
|
||||||
kind: SymbolKind.Directive,
|
kind: SymbolKind.Directive,
|
||||||
tsSymbol: symbol.tsSymbol,
|
tsSymbol: symbol.tsSymbol,
|
||||||
@ -250,6 +270,7 @@ export class SymbolBuilder {
|
|||||||
shimLocation: symbol.shimLocation,
|
shimLocation: symbol.shimLocation,
|
||||||
isComponent,
|
isComponent,
|
||||||
selector,
|
selector,
|
||||||
|
ngModule,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ ts_library(
|
|||||||
"//packages/compiler-cli/src/ngtsc/incremental",
|
"//packages/compiler-cli/src/ngtsc/incremental",
|
||||||
"//packages/compiler-cli/src/ngtsc/metadata",
|
"//packages/compiler-cli/src/ngtsc/metadata",
|
||||||
"//packages/compiler-cli/src/ngtsc/reflection",
|
"//packages/compiler-cli/src/ngtsc/reflection",
|
||||||
|
"//packages/compiler-cli/src/ngtsc/scope",
|
||||||
"//packages/compiler-cli/src/ngtsc/shims",
|
"//packages/compiler-cli/src/ngtsc/shims",
|
||||||
"//packages/compiler-cli/src/ngtsc/testing",
|
"//packages/compiler-cli/src/ngtsc/testing",
|
||||||
"//packages/compiler-cli/src/ngtsc/typecheck",
|
"//packages/compiler-cli/src/ngtsc/typecheck",
|
||||||
|
@ -11,10 +11,11 @@ import * as ts from 'typescript';
|
|||||||
|
|
||||||
import {absoluteFrom, AbsoluteFsPath, getSourceFileOrError, LogicalFileSystem} from '../../file_system';
|
import {absoluteFrom, AbsoluteFsPath, getSourceFileOrError, LogicalFileSystem} from '../../file_system';
|
||||||
import {TestFile} from '../../file_system/testing';
|
import {TestFile} from '../../file_system/testing';
|
||||||
import {AbsoluteModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, Reference, ReferenceEmitter} from '../../imports';
|
import {AbsoluteModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, Reexport, Reference, ReferenceEmitter} from '../../imports';
|
||||||
import {NOOP_INCREMENTAL_BUILD} from '../../incremental';
|
import {NOOP_INCREMENTAL_BUILD} from '../../incremental';
|
||||||
import {ClassPropertyMapping} from '../../metadata';
|
import {ClassPropertyMapping} from '../../metadata';
|
||||||
import {ClassDeclaration, isNamedClassDeclaration, TypeScriptReflectionHost} from '../../reflection';
|
import {ClassDeclaration, isNamedClassDeclaration, TypeScriptReflectionHost} from '../../reflection';
|
||||||
|
import {ComponentScopeReader, ScopeData} from '../../scope';
|
||||||
import {makeProgram} from '../../testing';
|
import {makeProgram} from '../../testing';
|
||||||
import {getRootDirs} from '../../util/src/typescript';
|
import {getRootDirs} from '../../util/src/typescript';
|
||||||
import {ProgramTypeCheckAdapter, TemplateTypeChecker, TypeCheckContext} from '../api';
|
import {ProgramTypeCheckAdapter, TemplateTypeChecker, TypeCheckContext} from '../api';
|
||||||
@ -404,9 +405,32 @@ export function setup(targets: TypeCheckingTarget[], overrides: {
|
|||||||
(programStrategy as any).supportsInlineOperations = overrides.inlining;
|
(programStrategy as any).supportsInlineOperations = overrides.inlining;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fakeScopeReader = {
|
||||||
|
getRequiresRemoteScope() {
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
// If there is a module with [className] + 'Module' in the same source file, returns
|
||||||
|
// `LocalModuleScope` with the ngModule class and empty arrays for everything else.
|
||||||
|
getScopeForComponent(clazz: ClassDeclaration) {
|
||||||
|
try {
|
||||||
|
const ngModule = getClass(clazz.getSourceFile(), `${clazz.name.getText()}Module`);
|
||||||
|
const stubScopeData = {directives: [], ngModules: [], pipes: []};
|
||||||
|
return {
|
||||||
|
ngModule,
|
||||||
|
compilation: stubScopeData,
|
||||||
|
reexports: [],
|
||||||
|
schemas: [],
|
||||||
|
exported: stubScopeData
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const templateTypeChecker = new TemplateTypeCheckerImpl(
|
const templateTypeChecker = new TemplateTypeCheckerImpl(
|
||||||
program, programStrategy, checkAdapter, fullConfig, emitter, reflectionHost, host,
|
program, programStrategy, checkAdapter, fullConfig, emitter, reflectionHost, host,
|
||||||
NOOP_INCREMENTAL_BUILD);
|
NOOP_INCREMENTAL_BUILD, fakeScopeReader);
|
||||||
return {
|
return {
|
||||||
templateTypeChecker,
|
templateTypeChecker,
|
||||||
program,
|
program,
|
||||||
|
@ -1304,7 +1304,11 @@ runInEachFileSystem(() => {
|
|||||||
fileName: dirFile,
|
fileName: dirFile,
|
||||||
source: `
|
source: `
|
||||||
export class TestDir {}
|
export class TestDir {}
|
||||||
|
// Allow the fake ComponentScopeReader to return a module for TestDir
|
||||||
|
export class TestDirModule {}
|
||||||
export class TestDir2 {}
|
export class TestDir2 {}
|
||||||
|
// Allow the fake ComponentScopeReader to return a module for TestDir2
|
||||||
|
export class TestDir2Module {}
|
||||||
export class TestDirAllDivs {}
|
export class TestDirAllDivs {}
|
||||||
`,
|
`,
|
||||||
templates: {},
|
templates: {},
|
||||||
@ -1327,6 +1331,15 @@ runInEachFileSystem(() => {
|
|||||||
const expectedSelectors = ['[dir]', '[dir2]', 'div'].sort();
|
const expectedSelectors = ['[dir]', '[dir2]', 'div'].sort();
|
||||||
const actualSelectors = symbol.directives.map(dir => dir.selector).sort();
|
const actualSelectors = symbol.directives.map(dir => dir.selector).sort();
|
||||||
expect(actualSelectors).toEqual(expectedSelectors);
|
expect(actualSelectors).toEqual(expectedSelectors);
|
||||||
|
|
||||||
|
// Testing this fully requires an integration test with a real `NgCompiler` (like in the
|
||||||
|
// Language Service, which uses the ngModule name for quick info). However, this path does
|
||||||
|
// assert that we are able to handle when the scope reader returns `null` or a class from
|
||||||
|
// the fake implementation.
|
||||||
|
const expectedModules = new Set([null, 'TestDirModule', 'TestDir2Module']);
|
||||||
|
const actualModules =
|
||||||
|
new Set(symbol.directives.map(dir => dir.ngModule?.name.getText() ?? null));
|
||||||
|
expect(actualModules).toEqual(expectedModules);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user