refactor(compiler-cli): API to get directives/pipes in scope (#39278)
This commit introduces two new methods to the TemplateTypeChecker, which retrieve the directives and pipes that are "in scope" for a given component template. The metadata returned by this API is minimal, but enough to power autocompletion of selectors and attributes in templates. PR Close #39278
This commit is contained in:
parent
01cc949722
commit
0ecdef9cfa
|
@ -10,6 +10,7 @@ import {AST, ParseError, TmplAstNode, TmplAstTemplate} from '@angular/compiler';
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
import {GlobalCompletion} from './completion';
|
import {GlobalCompletion} from './completion';
|
||||||
|
import {DirectiveInScope, PipeInScope} from './scope';
|
||||||
import {Symbol} from './symbols';
|
import {Symbol} from './symbols';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -101,6 +102,16 @@ export interface TemplateTypeChecker {
|
||||||
*/
|
*/
|
||||||
getGlobalCompletions(context: TmplAstTemplate|null, component: ts.ClassDeclaration):
|
getGlobalCompletions(context: TmplAstTemplate|null, component: ts.ClassDeclaration):
|
||||||
GlobalCompletion|null;
|
GlobalCompletion|null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get basic metadata on the directives which are in scope for the given component.
|
||||||
|
*/
|
||||||
|
getDirectivesInScope(component: ts.ClassDeclaration): DirectiveInScope[]|null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get basic metadata on the pipes which are in scope for the given component.
|
||||||
|
*/
|
||||||
|
getPipesInScope(component: ts.ClassDeclaration): PipeInScope[]|null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -10,4 +10,5 @@ export * from './api';
|
||||||
export * from './checker';
|
export * from './checker';
|
||||||
export * from './completion';
|
export * from './completion';
|
||||||
export * from './context';
|
export * from './context';
|
||||||
|
export * from './scope';
|
||||||
export * from './symbols';
|
export * from './symbols';
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google LLC All Rights Reserved.
|
||||||
|
*
|
||||||
|
* 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 * as ts from 'typescript';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metadata on a directive which is available in the scope of a template.
|
||||||
|
*/
|
||||||
|
export interface DirectiveInScope {
|
||||||
|
/**
|
||||||
|
* The `ts.Symbol` for the directive class.
|
||||||
|
*/
|
||||||
|
tsSymbol: ts.Symbol;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The selector for the directive or component.
|
||||||
|
*/
|
||||||
|
selector: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `true` if this directive is a component.
|
||||||
|
*/
|
||||||
|
isComponent: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metadata for a pipe which is available in the scope of a template.
|
||||||
|
*/
|
||||||
|
export interface PipeInScope {
|
||||||
|
/**
|
||||||
|
* The `ts.Symbol` for the pipe class.
|
||||||
|
*/
|
||||||
|
tsSymbol: ts.Symbol;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name of the pipe.
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
}
|
|
@ -11,6 +11,7 @@ import * as ts from 'typescript';
|
||||||
|
|
||||||
import {AbsoluteFsPath} from '../../file_system';
|
import {AbsoluteFsPath} from '../../file_system';
|
||||||
import {ClassDeclaration} from '../../reflection';
|
import {ClassDeclaration} from '../../reflection';
|
||||||
|
import {DirectiveInScope} from './scope';
|
||||||
|
|
||||||
export enum SymbolKind {
|
export enum SymbolKind {
|
||||||
Input,
|
Input,
|
||||||
|
@ -222,24 +223,15 @@ export interface TemplateSymbol {
|
||||||
* A representation of a directive/component whose selector matches a node in a component
|
* A representation of a directive/component whose selector matches a node in a component
|
||||||
* template.
|
* template.
|
||||||
*/
|
*/
|
||||||
export interface DirectiveSymbol {
|
export interface DirectiveSymbol extends DirectiveInScope {
|
||||||
kind: SymbolKind.Directive;
|
kind: SymbolKind.Directive;
|
||||||
|
|
||||||
/** The `ts.Type` for the class declaration. */
|
/** The `ts.Type` for the class declaration. */
|
||||||
tsType: ts.Type;
|
tsType: ts.Type;
|
||||||
|
|
||||||
/** The `ts.Symbol` for the class declaration. */
|
|
||||||
tsSymbol: ts.Symbol;
|
|
||||||
|
|
||||||
/** The location in the shim file for the variable that holds the type of the directive. */
|
/** The location in the shim file for the variable that holds the type of the directive. */
|
||||||
shimLocation: ShimLocation;
|
shimLocation: ShimLocation;
|
||||||
|
|
||||||
/** The selector for the `Directive` / `Component`. */
|
|
||||||
selector: string|null;
|
|
||||||
|
|
||||||
/** `true` if this `DirectiveSymbol` is for a @Component. */
|
|
||||||
isComponent: boolean;
|
|
||||||
|
|
||||||
/** The `NgModule` that this directive is declared in or `null` if it could not be determined. */
|
/** The `NgModule` that this directive is declared in or `null` if it could not be determined. */
|
||||||
ngModule: ClassDeclaration|null;
|
ngModule: ClassDeclaration|null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,11 +12,11 @@ import * as ts from 'typescript';
|
||||||
import {absoluteFromSourceFile, AbsoluteFsPath, getSourceFileOrError} from '../../file_system';
|
import {absoluteFromSourceFile, AbsoluteFsPath, getSourceFileOrError} from '../../file_system';
|
||||||
import {ReferenceEmitter} from '../../imports';
|
import {ReferenceEmitter} from '../../imports';
|
||||||
import {IncrementalBuild} from '../../incremental/api';
|
import {IncrementalBuild} from '../../incremental/api';
|
||||||
import {ReflectionHost} from '../../reflection';
|
import {isNamedClassDeclaration, ReflectionHost} from '../../reflection';
|
||||||
import {ComponentScopeReader} from '../../scope';
|
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 {CompletionKind, GlobalCompletion, OptimizeFor, ProgramTypeCheckAdapter, Symbol, TemplateId, TemplateTypeChecker, TypeCheckingConfig, TypeCheckingProgramStrategy, UpdateMode} from '../api';
|
import {CompletionKind, DirectiveInScope, GlobalCompletion, OptimizeFor, PipeInScope, ProgramTypeCheckAdapter, Symbol, TemplateId, TemplateTypeChecker, TypeCheckingConfig, TypeCheckingProgramStrategy, UpdateMode} from '../api';
|
||||||
import {TemplateDiagnostic} from '../diagnostics';
|
import {TemplateDiagnostic} from '../diagnostics';
|
||||||
|
|
||||||
import {ExpressionIdentifier, findFirstMatchingNode} from './comments';
|
import {ExpressionIdentifier, findFirstMatchingNode} from './comments';
|
||||||
|
@ -51,6 +51,16 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
|
||||||
*/
|
*/
|
||||||
private symbolBuilderCache = new Map<ts.ClassDeclaration, SymbolBuilder>();
|
private symbolBuilderCache = new Map<ts.ClassDeclaration, SymbolBuilder>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores directives and pipes that are in scope for each component.
|
||||||
|
*
|
||||||
|
* Unlike the other caches, the scope of a component is not affected by its template, so this
|
||||||
|
* cache does not need to be invalidate if the template is overridden. It will be destroyed when
|
||||||
|
* the `ts.Program` changes and the `TemplateTypeCheckerImpl` as a whole is destroyed and
|
||||||
|
* replaced.
|
||||||
|
*/
|
||||||
|
private scopeCache = new Map<ts.ClassDeclaration, ScopeData>();
|
||||||
|
|
||||||
private isComplete = false;
|
private isComplete = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -433,6 +443,73 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
|
||||||
this.symbolBuilderCache.set(component, builder);
|
this.symbolBuilderCache.set(component, builder);
|
||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getDirectivesInScope(component: ts.ClassDeclaration): DirectiveInScope[]|null {
|
||||||
|
const data = this.getScopeData(component);
|
||||||
|
if (data === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return data.directives;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPipesInScope(component: ts.ClassDeclaration): PipeInScope[]|null {
|
||||||
|
const data = this.getScopeData(component);
|
||||||
|
if (data === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return data.pipes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getScopeData(component: ts.ClassDeclaration): ScopeData|null {
|
||||||
|
if (this.scopeCache.has(component)) {
|
||||||
|
return this.scopeCache.get(component)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isNamedClassDeclaration(component)) {
|
||||||
|
throw new Error(`AssertionError: components must have names`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: ScopeData = {
|
||||||
|
directives: [],
|
||||||
|
pipes: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const scope = this.componentScopeReader.getScopeForComponent(component);
|
||||||
|
if (scope === null || scope === 'error') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeChecker = this.typeCheckingStrategy.getProgram().getTypeChecker();
|
||||||
|
for (const dir of scope.exported.directives) {
|
||||||
|
if (dir.selector === null) {
|
||||||
|
// Skip this directive, it can't be added to a template anyway.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const tsSymbol = typeChecker.getSymbolAtLocation(dir.ref.node.name);
|
||||||
|
if (tsSymbol === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
data.directives.push({
|
||||||
|
isComponent: dir.isComponent,
|
||||||
|
selector: dir.selector,
|
||||||
|
tsSymbol,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const pipe of scope.exported.pipes) {
|
||||||
|
const tsSymbol = typeChecker.getSymbolAtLocation(pipe.ref.node.name);
|
||||||
|
if (tsSymbol === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
data.pipes.push({
|
||||||
|
name: pipe.name,
|
||||||
|
tsSymbol,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.scopeCache.set(component, data);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertDiagnostic(
|
function convertDiagnostic(
|
||||||
|
@ -622,3 +699,11 @@ class SingleShimTypeCheckingHost extends SingleFileTypeCheckingHost {
|
||||||
return !this.fileData.shimData.has(shimPath);
|
return !this.fileData.shimData.has(shimPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cached scope information for a component.
|
||||||
|
*/
|
||||||
|
interface ScopeData {
|
||||||
|
directives: DirectiveInScope[];
|
||||||
|
pipes: PipeInScope[];
|
||||||
|
}
|
||||||
|
|
|
@ -129,12 +129,14 @@ export class SymbolBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
const ngModule = this.getDirectiveModule(symbol.tsSymbol.valueDeclaration);
|
const ngModule = this.getDirectiveModule(symbol.tsSymbol.valueDeclaration);
|
||||||
const selector = meta.selector ?? null;
|
if (meta.selector === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const isComponent = meta.isComponent ?? null;
|
const isComponent = meta.isComponent ?? null;
|
||||||
const directiveSymbol: DirectiveSymbol = {
|
const directiveSymbol: DirectiveSymbol = {
|
||||||
...symbol,
|
...symbol,
|
||||||
tsSymbol: symbol.tsSymbol,
|
tsSymbol: symbol.tsSymbol,
|
||||||
selector,
|
selector: meta.selector,
|
||||||
isComponent,
|
isComponent,
|
||||||
ngModule,
|
ngModule,
|
||||||
kind: SymbolKind.Directive
|
kind: SymbolKind.Directive
|
||||||
|
@ -256,7 +258,7 @@ export class SymbolBuilder {
|
||||||
// In either case, `_t1["index"]` or `_t1.index`, `node.expression` is _t1.
|
// In either case, `_t1["index"]` or `_t1.index`, `node.expression` is _t1.
|
||||||
// The retrieved symbol for _t1 will be the variable declaration.
|
// The retrieved symbol for _t1 will be the variable declaration.
|
||||||
const tsSymbol = this.getTypeChecker().getSymbolAtLocation(node.expression);
|
const tsSymbol = this.getTypeChecker().getSymbolAtLocation(node.expression);
|
||||||
if (tsSymbol === undefined || tsSymbol.declarations.length === 0) {
|
if (tsSymbol === undefined || tsSymbol.declarations.length === 0 || selector === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ import {AbsoluteModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy,
|
||||||
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 {ComponentScopeReader, LocalModuleScope, 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';
|
||||||
|
@ -355,6 +355,18 @@ export function setup(targets: TypeCheckingTarget[], overrides: {
|
||||||
]);
|
]);
|
||||||
const fullConfig = {...ALL_ENABLED_CONFIG, ...config};
|
const fullConfig = {...ALL_ENABLED_CONFIG, ...config};
|
||||||
|
|
||||||
|
// Map out the scope of each target component, which is needed for the ComponentScopeReader.
|
||||||
|
const scopeMap = new Map<ClassDeclaration, ScopeData>();
|
||||||
|
for (const target of targets) {
|
||||||
|
const sf = getSourceFileOrError(program, target.fileName);
|
||||||
|
const scope = makeScope(program, sf, target.declarations ?? []);
|
||||||
|
|
||||||
|
for (const className of Object.keys(target.templates)) {
|
||||||
|
const classDecl = getClass(sf, className);
|
||||||
|
scopeMap.set(classDecl, scope);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const checkAdapter = createTypeCheckAdapter((sf, ctx) => {
|
const checkAdapter = createTypeCheckAdapter((sf, ctx) => {
|
||||||
for (const target of targets) {
|
for (const target of targets) {
|
||||||
if (getSourceFileOrError(program, target.fileName) !== sf) {
|
if (getSourceFileOrError(program, target.fileName) !== sf) {
|
||||||
|
@ -405,27 +417,47 @@ export function setup(targets: TypeCheckingTarget[], overrides: {
|
||||||
(programStrategy as any).supportsInlineOperations = overrides.inlining;
|
(programStrategy as any).supportsInlineOperations = overrides.inlining;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fakeScopeReader = {
|
const fakeScopeReader: ComponentScopeReader = {
|
||||||
getRequiresRemoteScope() {
|
getRequiresRemoteScope() {
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
// If there is a module with [className] + 'Module' in the same source file, returns
|
// If there is a module with [className] + 'Module' in the same source file, that will be
|
||||||
// `LocalModuleScope` with the ngModule class and empty arrays for everything else.
|
// returned as the NgModule for the class.
|
||||||
getScopeForComponent(clazz: ClassDeclaration) {
|
getScopeForComponent(clazz: ClassDeclaration): LocalModuleScope |
|
||||||
try {
|
null {
|
||||||
const ngModule = getClass(clazz.getSourceFile(), `${clazz.name.getText()}Module`);
|
try {
|
||||||
const stubScopeData = {directives: [], ngModules: [], pipes: []};
|
const ngModule = getClass(clazz.getSourceFile(), `${clazz.name.getText()}Module`);
|
||||||
return {
|
|
||||||
ngModule,
|
if (!scopeMap.has(clazz)) {
|
||||||
compilation: stubScopeData,
|
// This class wasn't part of the target set of components with templates, but is
|
||||||
reexports: [],
|
// probably a declaration used in one of them. Return an empty scope.
|
||||||
schemas: [],
|
const emptyScope: ScopeData = {
|
||||||
exported: stubScopeData
|
directives: [],
|
||||||
};
|
ngModules: [],
|
||||||
} catch (e) {
|
pipes: [],
|
||||||
return null;
|
};
|
||||||
}
|
return {
|
||||||
}
|
ngModule,
|
||||||
|
compilation: emptyScope,
|
||||||
|
reexports: [],
|
||||||
|
schemas: [],
|
||||||
|
exported: emptyScope,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const scope = scopeMap.get(clazz)!;
|
||||||
|
|
||||||
|
return {
|
||||||
|
ngModule,
|
||||||
|
compilation: scope,
|
||||||
|
reexports: [],
|
||||||
|
schemas: [],
|
||||||
|
exported: scope,
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
// No NgModule was found for this class, so it has no scope.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const templateTypeChecker = new TemplateTypeCheckerImpl(
|
const templateTypeChecker = new TemplateTypeCheckerImpl(
|
||||||
|
@ -492,6 +524,55 @@ export function getClass(sf: ts.SourceFile, name: string): ClassDeclaration<ts.C
|
||||||
throw new Error(`Class ${name} not found in file: ${sf.fileName}: ${sf.text}`);
|
throw new Error(`Class ${name} not found in file: ${sf.fileName}: ${sf.text}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synthesize `ScopeData` metadata from an array of `TestDeclaration`s.
|
||||||
|
*/
|
||||||
|
function makeScope(program: ts.Program, sf: ts.SourceFile, decls: TestDeclaration[]): ScopeData {
|
||||||
|
const scope: ScopeData = {
|
||||||
|
ngModules: [],
|
||||||
|
directives: [],
|
||||||
|
pipes: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const decl of decls) {
|
||||||
|
let declSf = sf;
|
||||||
|
if (decl.file !== undefined) {
|
||||||
|
declSf = getSourceFileOrError(program, decl.file);
|
||||||
|
}
|
||||||
|
const declClass = getClass(declSf, decl.name);
|
||||||
|
|
||||||
|
if (decl.type === 'directive') {
|
||||||
|
scope.directives.push({
|
||||||
|
ref: new Reference(declClass),
|
||||||
|
baseClass: null,
|
||||||
|
name: decl.name,
|
||||||
|
selector: decl.selector,
|
||||||
|
queries: [],
|
||||||
|
inputs: decl.inputs !== undefined ? ClassPropertyMapping.fromMappedObject(decl.inputs) :
|
||||||
|
ClassPropertyMapping.empty(),
|
||||||
|
outputs: decl.outputs !== undefined ? ClassPropertyMapping.fromMappedObject(decl.outputs) :
|
||||||
|
ClassPropertyMapping.empty(),
|
||||||
|
isComponent: decl.isComponent ?? false,
|
||||||
|
exportAs: decl.exportAs ?? null,
|
||||||
|
ngTemplateGuards: decl.ngTemplateGuards ?? [],
|
||||||
|
hasNgTemplateContextGuard: decl.hasNgTemplateContextGuard ?? false,
|
||||||
|
coercedInputFields: new Set(decl.coercedInputFields ?? []),
|
||||||
|
restrictedInputFields: new Set(decl.restrictedInputFields ?? []),
|
||||||
|
stringLiteralInputFields: new Set(decl.stringLiteralInputFields ?? []),
|
||||||
|
undeclaredInputFields: new Set(decl.undeclaredInputFields ?? []),
|
||||||
|
isGeneric: decl.isGeneric ?? false,
|
||||||
|
});
|
||||||
|
} else if (decl.type === 'pipe') {
|
||||||
|
scope.pipes.push({
|
||||||
|
ref: new Reference(declClass),
|
||||||
|
name: decl.pipeName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return scope;
|
||||||
|
}
|
||||||
|
|
||||||
class FakeEnvironment /* implements Environment */ {
|
class FakeEnvironment /* implements Environment */ {
|
||||||
constructor(readonly config: TypeCheckingConfig) {}
|
constructor(readonly config: TypeCheckingConfig) {}
|
||||||
|
|
||||||
|
|
|
@ -98,6 +98,45 @@ runInEachFileSystem(() => {
|
||||||
expect(Array.from(afterReset.templateContext.keys())).toEqual(['foo']);
|
expect(Array.from(afterReset.templateContext.keys())).toEqual(['foo']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('TemplateTypeChecker scopes', () => {
|
||||||
|
it('should get directives and pipes in scope for a component', () => {
|
||||||
|
const MAIN_TS = absoluteFrom('/main.ts');
|
||||||
|
const {program, templateTypeChecker} = setup([{
|
||||||
|
fileName: MAIN_TS,
|
||||||
|
templates: {
|
||||||
|
'SomeCmp': 'Not important',
|
||||||
|
},
|
||||||
|
declarations: [
|
||||||
|
{
|
||||||
|
type: 'directive',
|
||||||
|
file: MAIN_TS,
|
||||||
|
name: 'OtherDir',
|
||||||
|
selector: 'other-dir',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'pipe',
|
||||||
|
file: MAIN_TS,
|
||||||
|
name: 'OtherPipe',
|
||||||
|
pipeName: 'otherPipe',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
source: `
|
||||||
|
export class SomeCmp {}
|
||||||
|
export class OtherDir {}
|
||||||
|
export class OtherPipe {}
|
||||||
|
export class SomeCmpModule {}
|
||||||
|
`
|
||||||
|
}]);
|
||||||
|
const sf = getSourceFileOrError(program, MAIN_TS);
|
||||||
|
const SomeCmp = getClass(sf, 'SomeCmp');
|
||||||
|
|
||||||
|
const directives = templateTypeChecker.getDirectivesInScope(SomeCmp) ?? [];
|
||||||
|
const pipes = templateTypeChecker.getPipesInScope(SomeCmp) ?? [];
|
||||||
|
expect(directives.map(dir => dir.selector)).toEqual(['other-dir']);
|
||||||
|
expect(pipes.map(pipe => pipe.name)).toEqual(['otherPipe']);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function setupCompletions(
|
function setupCompletions(
|
||||||
|
|
Loading…
Reference in New Issue