refactor(compiler-cli): add getTemplateOfComponent to TemplateTypeChecker (#38355)

This commit adds a `getTemplateOfComponent` method to the
`TemplateTypeChecker` API, which retrieves the actual nodes parsed and used
by the compiler for template type-checking. This is advantageous for the
language service, which may need to query other APIs in
`TemplateTypeChecker` that require the same nodes used to bind the template
while generating the TCB.

Fixes #38352

PR Close #38355
This commit is contained in:
Alex Rickabaugh 2020-08-05 16:13:12 -07:00 committed by Misko Hevery
parent 1ec609946f
commit 0b54c0c6b4
4 changed files with 104 additions and 3 deletions

View File

@ -28,6 +28,14 @@ export interface TemplateTypeChecker {
*/ */
resetOverrides(): void; resetOverrides(): void;
/**
* Retrieve the template in use for the given component.
*
* If the template has been overridden via `overrideComponentTemplate`, this will retrieve the
* overridden template nodes.
*/
getTemplate(component: ts.ClassDeclaration): TmplAstNode[]|null;
/** /**
* Provide a new template string that will be used in place of the user-defined template when * Provide a new template string that will be used in place of the user-defined template when
* checking or operating on the given component. * checking or operating on the given component.

View File

@ -48,6 +48,29 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
} }
} }
getTemplate(component: ts.ClassDeclaration): TmplAstNode[]|null {
this.ensureShimForComponent(component);
const sf = component.getSourceFile();
const sfPath = absoluteFromSourceFile(sf);
const shimPath = this.typeCheckingStrategy.shimPathForComponent(component);
const fileRecord = this.getFileData(sfPath);
if (!fileRecord.shimData.has(shimPath)) {
return [];
}
const templateId = fileRecord.sourceManager.getTemplateId(component);
const shimRecord = fileRecord.shimData.get(shimPath)!;
if (!shimRecord.templates.has(templateId)) {
return null;
}
return shimRecord.templates.get(templateId)!.template;
}
overrideComponentTemplate(component: ts.ClassDeclaration, template: string): overrideComponentTemplate(component: ts.ClassDeclaration, template: string):
{nodes: TmplAstNode[], errors?: ParseError[]} { {nodes: TmplAstNode[], errors?: ParseError[]} {
const {nodes, errors} = parseTemplate(template, 'override.html', { const {nodes, errors} = parseTemplate(template, 'override.html', {

View File

@ -6,14 +6,14 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {ParseSourceFile, R3TargetBinder, SchemaMetadata, TmplAstNode} from '@angular/compiler'; import {BoundTarget, ParseSourceFile, R3TargetBinder, SchemaMetadata, TmplAstNode} from '@angular/compiler';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {absoluteFromSourceFile, AbsoluteFsPath} from '../../file_system'; import {absoluteFromSourceFile, AbsoluteFsPath} from '../../file_system';
import {NoopImportRewriter, Reference, ReferenceEmitter} from '../../imports'; import {NoopImportRewriter, Reference, ReferenceEmitter} from '../../imports';
import {ClassDeclaration, ReflectionHost} from '../../reflection'; import {ClassDeclaration, ReflectionHost} from '../../reflection';
import {ImportManager} from '../../translator'; import {ImportManager} from '../../translator';
import {ComponentToShimMappingStrategy, TemplateSourceMapping, TypeCheckableDirectiveMeta, TypeCheckBlockMetadata, TypeCheckContext, TypeCheckingConfig, TypeCtorMetadata} from '../api'; import {ComponentToShimMappingStrategy, TemplateId, TemplateSourceMapping, TypeCheckableDirectiveMeta, TypeCheckBlockMetadata, TypeCheckContext, TypeCheckingConfig, TypeCtorMetadata} from '../api';
import {TemplateDiagnostic} from './diagnostics'; import {TemplateDiagnostic} from './diagnostics';
import {DomSchemaChecker, RegistryDomSchemaChecker} from './dom'; import {DomSchemaChecker, RegistryDomSchemaChecker} from './dom';
@ -41,6 +41,28 @@ export interface ShimTypeCheckingData {
* Whether any inline operations for the input file were required to generate this shim. * Whether any inline operations for the input file were required to generate this shim.
*/ */
hasInlines: boolean; hasInlines: boolean;
/**
* Map of `TemplateId` to information collected about the template during the template
* type-checking process.
*/
templates: Map<TemplateId, TemplateData>;
}
/**
* Data tracked for each template processed by the template type-checking system.
*/
export interface TemplateData {
/**
* Template nodes for which the TCB was generated.
*/
template: TmplAstNode[];
/**
* `BoundTarget` which was used to generate the TCB, and contains bindings for the associated
* template nodes.
*/
boundTarget: BoundTarget<TypeCheckableDirectiveMeta>;
} }
/** /**
@ -79,6 +101,12 @@ export interface PendingShimData {
* Shim file in the process of being generated. * Shim file in the process of being generated.
*/ */
file: TypeCheckFile; file: TypeCheckFile;
/**
* Map of `TemplateId` to information collected about the template as it's ingested.
*/
templates: Map<TemplateId, TemplateData>;
} }
/** /**
@ -195,6 +223,7 @@ export class TypeCheckContextImpl implements TypeCheckContext {
const fileData = this.dataForFile(ref.node.getSourceFile()); const fileData = this.dataForFile(ref.node.getSourceFile());
const shimData = this.pendingShimForComponent(ref.node); const shimData = this.pendingShimForComponent(ref.node);
const boundTarget = binder.bind({template}); const boundTarget = binder.bind({template});
// Get all of the directives used in the template and record type constructors for all of them. // Get all of the directives used in the template and record type constructors for all of them.
for (const dir of boundTarget.getUsedDirectives()) { for (const dir of boundTarget.getUsedDirectives()) {
const dirRef = dir.ref as Reference<ClassDeclaration<ts.ClassDeclaration>>; const dirRef = dir.ref as Reference<ClassDeclaration<ts.ClassDeclaration>>;
@ -221,6 +250,11 @@ export class TypeCheckContextImpl implements TypeCheckContext {
}); });
} }
} }
const templateId = fileData.sourceManager.getTemplateId(ref.node);
shimData.templates.set(templateId, {
template,
boundTarget,
});
const tcbRequiresInline = requiresInlineTypeCheckBlock(ref.node); const tcbRequiresInline = requiresInlineTypeCheckBlock(ref.node);
@ -231,7 +265,6 @@ export class TypeCheckContextImpl implements TypeCheckContext {
// and inlining would be required. // and inlining would be required.
// Record diagnostics to indicate the issues with this template. // Record diagnostics to indicate the issues with this template.
const templateId = fileData.sourceManager.getTemplateId(ref.node);
if (tcbRequiresInline) { if (tcbRequiresInline) {
shimData.oobRecorder.requiresInlineTcb(templateId, ref.node); shimData.oobRecorder.requiresInlineTcb(templateId, ref.node);
} }
@ -348,6 +381,7 @@ export class TypeCheckContextImpl implements TypeCheckContext {
], ],
hasInlines: pendingFileData.hasInlines, hasInlines: pendingFileData.hasInlines,
path: pendingShimData.file.fileName, path: pendingShimData.file.fileName,
templates: pendingShimData.templates,
}); });
updates.set(pendingShimData.file.fileName, pendingShimData.file.render()); updates.set(pendingShimData.file.fileName, pendingShimData.file.render());
} }
@ -380,6 +414,7 @@ export class TypeCheckContextImpl implements TypeCheckContext {
oobRecorder: new OutOfBandDiagnosticRecorderImpl(fileData.sourceManager), oobRecorder: new OutOfBandDiagnosticRecorderImpl(fileData.sourceManager),
file: new TypeCheckFile( file: new TypeCheckFile(
shimPath, this.config, this.refEmitter, this.reflector, this.compilerHost), shimPath, this.config, this.refEmitter, this.reflector, this.compilerHost),
templates: new Map<TemplateId, TemplateData>(),
}); });
} }
return fileData.shimData.get(shimPath)!; return fileData.shimData.get(shimPath)!;

View File

@ -353,5 +353,40 @@ runInEachFileSystem(os => {
expect(diags2[0].messageText).toContain('invalid-element-b'); expect(diags2[0].messageText).toContain('invalid-element-b');
expect(diags2[0].messageText).not.toContain('invalid-element-a'); expect(diags2[0].messageText).not.toContain('invalid-element-a');
}); });
describe('getTemplateOfComponent()', () => {
it('should provide access to a component\'s real template', () => {
const fileName = absoluteFrom('/main.ts');
const {program, templateTypeChecker} = setup([{
fileName,
templates: {
'Cmp': '<div>Template</div>',
},
}]);
const cmp = getClass(getSourceFileOrError(program, fileName), 'Cmp');
const nodes = templateTypeChecker.getTemplate(cmp)!;
expect(nodes).not.toBeNull();
expect(nodes[0].sourceSpan.start.file.content).toBe('<div>Template</div>');
});
it('should provide access to an overridden template', () => {
const fileName = absoluteFrom('/main.ts');
const {program, templateTypeChecker} = setup([{
fileName,
templates: {
'Cmp': '<div>Template</div>',
},
}]);
const cmp = getClass(getSourceFileOrError(program, fileName), 'Cmp');
templateTypeChecker.overrideComponentTemplate(cmp, '<div>Overridden</div>');
templateTypeChecker.getDiagnosticsForComponent(cmp);
const nodes = templateTypeChecker.getTemplate(cmp)!;
expect(nodes).not.toBeNull();
expect(nodes[0].sourceSpan.start.file.content).toBe('<div>Overridden</div>');
});
});
}); });
}); });