Skip to content

Commit

Permalink
refactor(compiler): Add getPotentialPipes API method. (#48090)
Browse files Browse the repository at this point in the history
`getPotentialPipes` returns possible pipes which can be used in the provided context, whether already in scope or requiring an import.

This is necessary to implement auto-import support for pipes in the language service.

PR Close #48090
  • Loading branch information
dylhunn committed Nov 17, 2022
1 parent b16d332 commit ef6dbc8
Show file tree
Hide file tree
Showing 9 changed files with 90 additions and 27 deletions.
2 changes: 1 addition & 1 deletion packages/compiler-cli/src/ngtsc/metadata/src/api.ts
Expand Up @@ -254,7 +254,7 @@ export interface MetadataReader {
* A MetadataReader which also allows access to the set of all known directive classes.
*/
export interface MetadataReaderWithIndex extends MetadataReader {
getKnownDirectives(): Iterable<ClassDeclaration>;
getKnown(kind: MetaKind): Iterable<ClassDeclaration>;
}

/**
Expand Down
13 changes: 10 additions & 3 deletions packages/compiler-cli/src/ngtsc/metadata/src/registry.ts
Expand Up @@ -9,7 +9,7 @@
import {Reference} from '../../imports';
import {ClassDeclaration} from '../../reflection';

import {DirectiveMeta, MetadataReaderWithIndex, MetadataRegistry, NgModuleMeta, PipeMeta} from './api';
import {DirectiveMeta, MetadataReaderWithIndex, MetadataRegistry, MetaKind, NgModuleMeta, PipeMeta} from './api';

/**
* A registry of directive, pipe, and module metadata for types defined in the current compilation
Expand Down Expand Up @@ -40,8 +40,15 @@ export class LocalMetadataRegistry implements MetadataRegistry, MetadataReaderWi
this.pipes.set(meta.ref.node, meta);
}

getKnownDirectives(): Iterable<ClassDeclaration> {
return this.directives.keys();
getKnown(kind: MetaKind): Iterable<ClassDeclaration> {
switch (kind) {
case MetaKind.Directive:
return this.directives.keys();
case MetaKind.Pipe:
return this.pipes.keys();
case MetaKind.NgModule:
return this.ngModules.keys();
}
}
}

Expand Down
4 changes: 2 additions & 2 deletions packages/compiler-cli/src/ngtsc/typecheck/api/checker.ts
Expand Up @@ -135,9 +135,9 @@ export interface TemplateTypeChecker {
getPotentialTemplateDirectives(component: ts.ClassDeclaration): PotentialDirective[];

/**
* Get basic metadata on the pipes which are in scope for the given component.
* Get basic metadata on the pipes which are in scope or can be imported for the given component.
*/
getPipesInScope(component: ts.ClassDeclaration): PotentialPipe[]|null;
getPotentialPipes(component: ts.ClassDeclaration): PotentialPipe[];

/**
* Retrieve a `Map` of potential template element tags, to either the `PotentialDirective` that
Expand Down
2 changes: 2 additions & 0 deletions packages/compiler-cli/src/ngtsc/typecheck/api/scope.ts
Expand Up @@ -72,6 +72,8 @@ export interface PotentialDirective {
* Metadata for a pipe which is available in a template.
*/
export interface PotentialPipe {
ref: Reference<ClassDeclaration>;

/**
* The `ts.Symbol` for the pipe class.
*/
Expand Down
51 changes: 34 additions & 17 deletions packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts
Expand Up @@ -13,7 +13,7 @@ import {ErrorCode, ngErrorCode} from '../../diagnostics';
import {absoluteFrom, absoluteFromSourceFile, AbsoluteFsPath, getSourceFileOrError} from '../../file_system';
import {Reference, ReferenceEmitKind, ReferenceEmitter} from '../../imports';
import {IncrementalBuild} from '../../incremental/api';
import {DirectiveMeta, MetadataReader, MetadataReaderWithIndex, MetaKind, NgModuleMeta} from '../../metadata';
import {DirectiveMeta, MetadataReader, MetadataReaderWithIndex, MetaKind, NgModuleMeta, PipeMeta} from '../../metadata';
import {PerfCheckpoint, PerfEvent, PerfPhase, PerfRecorder} from '../../perf';
import {ProgramDriver, UpdateMode} from '../../program_driver';
import {ClassDeclaration, DeclarationNode, isNamedClassDeclaration, ReflectionHost} from '../../reflection';
Expand All @@ -31,7 +31,6 @@ import {TemplateSourceManager} from './source';
import {findTypeCheckBlock, getTemplateMapping, TemplateSourceResolver} from './tcb_util';
import {SymbolBuilder} from './template_symbol_builder';


const REGISTRY = new DomElementSchemaRegistry();
/**
* Primary template type-checking engine, which performs type-checking using a
Expand Down Expand Up @@ -561,7 +560,7 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
// Any additional directives found from the global registry can be used, but are not in scope.
// In the future, we can also walk other registries for .d.ts files, or traverse the
// import/export graph.
for (const directiveClass of this.localMetaReader.getKnownDirectives()) {
for (const directiveClass of this.localMetaReader.getKnown(MetaKind.Directive)) {
const directiveMeta = this.metaReader.getDirectiveMetadata(new Reference(directiveClass));
if (directiveMeta === null) continue;
if (resultingDirectives.has(directiveClass)) continue;
Expand All @@ -572,12 +571,23 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
return Array.from(resultingDirectives.values());
}

getPipesInScope(component: ts.ClassDeclaration): PotentialPipe[]|null {
const data = this.getScopeData(component);
if (data === null) {
return null;
getPotentialPipes(component: ts.ClassDeclaration): PotentialPipe[] {
// Very similar to the above `getPotentialTemplateDirectives`, but on pipes.
const typeChecker = this.programDriver.getProgram().getTypeChecker();
const inScopePipes = this.getScopeData(component)?.pipes ?? [];
const resultingPipes = new Map<ClassDeclaration<DeclarationNode>, PotentialPipe>();
for (const p of inScopePipes) {
resultingPipes.set(p.ref.node, p);
}
for (const pipeClass of this.localMetaReader.getKnown(MetaKind.Pipe)) {
const pipeMeta = this.metaReader.getPipeMetadata(new Reference(pipeClass));
if (pipeMeta === null) continue;
if (resultingPipes.has(pipeClass)) continue;
const withScope = this.scopeDataOfPipeMeta(typeChecker, pipeMeta);
if (withScope === null) continue;
resultingPipes.set(pipeClass, {...withScope, isInScope: false});
}
return data.pipes;
return Array.from(resultingPipes.values());
}

getDirectiveMetadata(dir: ts.ClassDeclaration): TypeCheckableDirectiveMeta|null {
Expand Down Expand Up @@ -747,15 +757,9 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
if (dirScope === null) continue;
data.directives.push({...dirScope, isInScope: true});
} else if (dep.kind === MetaKind.Pipe) {
const tsSymbol = typeChecker.getSymbolAtLocation(dep.ref.node.name);
if (tsSymbol === undefined) {
continue;
}
data.pipes.push({
name: dep.name,
tsSymbol,
isInScope: true,
});
const pipeScope = this.scopeDataOfPipeMeta(typeChecker, dep);
if (pipeScope === null) continue;
data.pipes.push({...pipeScope, isInScope: true});
}
}

Expand Down Expand Up @@ -789,6 +793,19 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
ngModule,
};
}

private scopeDataOfPipeMeta(typeChecker: ts.TypeChecker, dep: PipeMeta):
Omit<PotentialPipe, 'isInScope'>|null {
const tsSymbol = typeChecker.getSymbolAtLocation(dep.ref.node.name);
if (tsSymbol === undefined) {
return null;
}
return {
ref: dep.ref,
name: dep.name,
tsSymbol,
};
}
}

function convertDiagnostic(
Expand Down
Expand Up @@ -106,7 +106,7 @@ runInEachFileSystem(() => {

let directives = templateTypeChecker.getPotentialTemplateDirectives(SomeCmp) ?? [];
directives = directives.filter(d => d.isInScope);
const pipes = templateTypeChecker.getPipesInScope(SomeCmp) ?? [];
const pipes = templateTypeChecker.getPotentialPipes(SomeCmp) ?? [];
expect(directives.map(dir => dir.selector)).toEqual(['other-dir']);
expect(pipes.map(pipe => pipe.name)).toEqual(['otherPipe']);
});
Expand Down
9 changes: 7 additions & 2 deletions packages/compiler-cli/src/ngtsc/typecheck/testing/index.ts
Expand Up @@ -577,8 +577,13 @@ function getFakeMetadataReader(fakeMetadataRegistry: Map<any, DirectiveMeta|null
null {
return fakeMetadataRegistry.get(node.debugName) ?? null;
},
getKnownDirectives(): Iterable<ClassDeclaration> {
return fakeMetadataRegistry.keys();
getKnown(kind: MetaKind): Iterable<ClassDeclaration> {
switch (kind) {
case MetaKind.Directive:
return fakeMetadataRegistry.keys();
default:
return [];
}
}
} as MetadataReaderWithIndex;
}
Expand Down
31 changes: 31 additions & 0 deletions packages/compiler-cli/test/ngtsc/ls_typecheck_helpers_spec.ts
Expand Up @@ -202,6 +202,37 @@ runInEachFileSystem(() => {
});
});

describe('can retrieve candidate pipes` ', () => {
it('which are out of scope', () => {
env.write('one.ts', `
import {Pipe} from '@angular/core';
@Pipe({
name: 'foo-pipe',
standalone: true,
})
export class OnePipe {
}
`);

env.write('two.ts', `
import {Component} from '@angular/core';
@Component({
standalone: true,
selector: 'two-cmp',
template: '<div></div>',
})
export class TwoCmp {}
`);
const {program, checker} = env.driveTemplateTypeChecker();
const sf = program.getSourceFile(_('/one.ts'));
expect(sf).not.toBeNull();
const pipes = checker.getPotentialPipes(getClass(sf!, 'OnePipe'));
expect(pipes.map(p => p.name)).toContain('foo-pipe');
});
});

describe('can generate imports` ', () => {
it('for out of scope standalone components', () => {
env.write('one.ts', `
Expand Down
3 changes: 2 additions & 1 deletion packages/language-service/src/completions.ts
Expand Up @@ -842,7 +842,8 @@ export class CompletionBuilder<N extends TmplAstNode|AST> {

private getPipeCompletions(this: PipeCompletionBuilder):
ts.WithMetadata<ts.CompletionInfo>|undefined {
const pipes = this.templateTypeChecker.getPipesInScope(this.component);
const pipes =
this.templateTypeChecker.getPotentialPipes(this.component).filter(p => p.isInScope);
if (pipes === null) {
return undefined;
}
Expand Down

0 comments on commit ef6dbc8

Please sign in to comment.