fix(ivy): capture template source mapping details during preanalysis (#32544)
Prior to this change, the template source mapping details were always built during the analysis phase, under the assumption that pre-analysed templates would always correspond with external templates. This has turned out to be a false assumption, as inline templates are also pre-analyzed to be able to preload any stylesheets included in the template. This commit fixes the bug by capturing the template source mapping details at the moment the template is parsed, which is either during the preanalysis phase when preloading is available, or during the analysis phase when preloading is not supported. Tests have been added to exercise the template error mapping in asynchronous compilations where preloading is enabled, similar to how the CLI performs compilations. Fixes #32538 PR Close #32544
This commit is contained in:
parent
a391aebbcf
commit
a64eded521
|
@ -180,67 +180,32 @@ export class ComponentDecoratorHandler implements
|
||||||
let templateSourceMapping: TemplateSourceMapping;
|
let templateSourceMapping: TemplateSourceMapping;
|
||||||
if (this.preanalyzeTemplateCache.has(node)) {
|
if (this.preanalyzeTemplateCache.has(node)) {
|
||||||
// The template was parsed in preanalyze. Use it and delete it to save memory.
|
// The template was parsed in preanalyze. Use it and delete it to save memory.
|
||||||
const template = this.preanalyzeTemplateCache.get(node) !;
|
const preanalyzed = this.preanalyzeTemplateCache.get(node) !;
|
||||||
this.preanalyzeTemplateCache.delete(node);
|
this.preanalyzeTemplateCache.delete(node);
|
||||||
|
|
||||||
parseTemplate = template.parseTemplate;
|
parseTemplate = preanalyzed.parseTemplate;
|
||||||
|
templateSourceMapping = preanalyzed.templateSourceMapping;
|
||||||
// A pre-analyzed template is always an external mapping.
|
|
||||||
templateSourceMapping = {
|
|
||||||
type: 'external',
|
|
||||||
componentClass: node,
|
|
||||||
node: component.get('templateUrl') !,
|
|
||||||
template: template.template,
|
|
||||||
templateUrl: template.templateUrl,
|
|
||||||
};
|
|
||||||
} else {
|
} else {
|
||||||
// The template was not already parsed. Either there's a templateUrl, or an inline template.
|
// The template was not already parsed. Either there's a templateUrl, or an inline template.
|
||||||
if (component.has('templateUrl')) {
|
if (component.has('templateUrl')) {
|
||||||
const templateUrlExpr = component.get('templateUrl') !;
|
const templateUrlExpr = component.get('templateUrl') !;
|
||||||
const evalTemplateUrl = this.evaluator.evaluate(templateUrlExpr);
|
const templateUrl = this.evaluator.evaluate(templateUrlExpr);
|
||||||
if (typeof evalTemplateUrl !== 'string') {
|
if (typeof templateUrl !== 'string') {
|
||||||
throw new FatalDiagnosticError(
|
throw new FatalDiagnosticError(
|
||||||
ErrorCode.VALUE_HAS_WRONG_TYPE, templateUrlExpr, 'templateUrl must be a string');
|
ErrorCode.VALUE_HAS_WRONG_TYPE, templateUrlExpr, 'templateUrl must be a string');
|
||||||
}
|
}
|
||||||
const templateUrl = this.resourceLoader.resolve(evalTemplateUrl, containingFile);
|
const resourceUrl = this.resourceLoader.resolve(templateUrl, containingFile);
|
||||||
const templateStr = this.resourceLoader.load(templateUrl);
|
const external =
|
||||||
this.resourceDependencies.recordResourceDependency(node.getSourceFile(), templateUrl);
|
this._extractExternalTemplate(node, component, templateUrlExpr, resourceUrl);
|
||||||
|
|
||||||
parseTemplate = (options?: ParseTemplateOptions) => this._parseTemplate(
|
parseTemplate = external.parseTemplate;
|
||||||
component, templateStr, sourceMapUrl(templateUrl), /* templateRange */ undefined,
|
templateSourceMapping = external.templateSourceMapping;
|
||||||
/* escapedString */ false, options);
|
|
||||||
templateSourceMapping = {
|
|
||||||
type: 'external',
|
|
||||||
componentClass: node,
|
|
||||||
node: templateUrlExpr,
|
|
||||||
template: templateStr,
|
|
||||||
templateUrl: templateUrl,
|
|
||||||
};
|
|
||||||
} else {
|
} else {
|
||||||
// Expect an inline template to be present.
|
// Expect an inline template to be present.
|
||||||
const inlineTemplate = this._extractInlineTemplate(component, containingFile);
|
const inline = this._extractInlineTemplate(node, decorator, component, containingFile);
|
||||||
if (inlineTemplate === null) {
|
|
||||||
throw new FatalDiagnosticError(
|
parseTemplate = inline.parseTemplate;
|
||||||
ErrorCode.COMPONENT_MISSING_TEMPLATE, decorator.node,
|
templateSourceMapping = inline.templateSourceMapping;
|
||||||
'component is missing a template');
|
|
||||||
}
|
|
||||||
const {templateStr, templateUrl, templateRange, escapedString} = inlineTemplate;
|
|
||||||
parseTemplate = (options?: ParseTemplateOptions) => this._parseTemplate(
|
|
||||||
component, templateStr, templateUrl, templateRange, escapedString, options);
|
|
||||||
if (escapedString) {
|
|
||||||
templateSourceMapping = {
|
|
||||||
type: 'direct',
|
|
||||||
node:
|
|
||||||
component.get('template') !as(ts.StringLiteral | ts.NoSubstitutionTemplateLiteral),
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
templateSourceMapping = {
|
|
||||||
type: 'indirect',
|
|
||||||
node: component.get('template') !,
|
|
||||||
componentClass: node,
|
|
||||||
template: templateStr,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const template = parseTemplate();
|
const template = parseTemplate();
|
||||||
|
@ -589,7 +554,7 @@ export class ComponentDecoratorHandler implements
|
||||||
}
|
}
|
||||||
|
|
||||||
private _preloadAndParseTemplate(
|
private _preloadAndParseTemplate(
|
||||||
node: ts.Declaration, decorator: Decorator, component: Map<string, ts.Expression>,
|
node: ClassDeclaration, decorator: Decorator, component: Map<string, ts.Expression>,
|
||||||
containingFile: string): Promise<ParsedTemplate|null> {
|
containingFile: string): Promise<ParsedTemplate|null> {
|
||||||
if (component.has('templateUrl')) {
|
if (component.has('templateUrl')) {
|
||||||
// Extract the templateUrl and preload it.
|
// Extract the templateUrl and preload it.
|
||||||
|
@ -606,50 +571,64 @@ export class ComponentDecoratorHandler implements
|
||||||
// URLs to resolve.
|
// URLs to resolve.
|
||||||
if (templatePromise !== undefined) {
|
if (templatePromise !== undefined) {
|
||||||
return templatePromise.then(() => {
|
return templatePromise.then(() => {
|
||||||
const templateStr = this.resourceLoader.load(resourceUrl);
|
const {parseTemplate, templateSourceMapping} =
|
||||||
this.resourceDependencies.recordResourceDependency(node.getSourceFile(), resourceUrl);
|
this._extractExternalTemplate(node, component, templateUrlExpr, resourceUrl);
|
||||||
const parseTemplate = (options?: ParseTemplateOptions) => this._parseTemplate(
|
|
||||||
component, templateStr, sourceMapUrl(resourceUrl),
|
|
||||||
/* templateRange */ undefined,
|
|
||||||
/* escapedString */ false, options);
|
|
||||||
const template = parseTemplate();
|
const template = parseTemplate();
|
||||||
this.preanalyzeTemplateCache.set(node, {...template, parseTemplate});
|
this.preanalyzeTemplateCache.set(
|
||||||
|
node, {...template, parseTemplate, templateSourceMapping});
|
||||||
return template;
|
return template;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return Promise.resolve(null);
|
return Promise.resolve(null);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const inlineTemplate = this._extractInlineTemplate(component, containingFile);
|
const {parseTemplate, templateSourceMapping} =
|
||||||
if (inlineTemplate === null) {
|
this._extractInlineTemplate(node, decorator, component, containingFile);
|
||||||
throw new FatalDiagnosticError(
|
|
||||||
ErrorCode.COMPONENT_MISSING_TEMPLATE, decorator.node,
|
|
||||||
'component is missing a template');
|
|
||||||
}
|
|
||||||
|
|
||||||
const {templateStr, templateUrl, escapedString, templateRange} = inlineTemplate;
|
|
||||||
const parseTemplate = (options?: ParseTemplateOptions) => this._parseTemplate(
|
|
||||||
component, templateStr, templateUrl, templateRange, escapedString, options);
|
|
||||||
const template = parseTemplate();
|
const template = parseTemplate();
|
||||||
this.preanalyzeTemplateCache.set(node, {...template, parseTemplate});
|
this.preanalyzeTemplateCache.set(node, {...template, parseTemplate, templateSourceMapping});
|
||||||
return Promise.resolve(template);
|
return Promise.resolve(template);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _extractInlineTemplate(component: Map<string, ts.Expression>, containingFile: string): {
|
private _extractExternalTemplate(
|
||||||
templateStr: string,
|
node: ClassDeclaration, component: Map<string, ts.Expression>, templateUrlExpr: ts.Expression,
|
||||||
templateUrl: string,
|
resourceUrl: string): {
|
||||||
templateRange: LexerRange|undefined,
|
parseTemplate: (options?: ParseTemplateOptions) => ParsedTemplate;
|
||||||
escapedString: boolean
|
templateSourceMapping: TemplateSourceMapping
|
||||||
}|null {
|
} {
|
||||||
// If there is no inline template, then return null.
|
const templateStr = this.resourceLoader.load(resourceUrl);
|
||||||
|
this.resourceDependencies.recordResourceDependency(node.getSourceFile(), resourceUrl);
|
||||||
|
const parseTemplate = (options?: ParseTemplateOptions) => this._parseTemplate(
|
||||||
|
component, templateStr, sourceMapUrl(resourceUrl),
|
||||||
|
/* templateRange */ undefined,
|
||||||
|
/* escapedString */ false, options);
|
||||||
|
const templateSourceMapping: TemplateSourceMapping = {
|
||||||
|
type: 'external',
|
||||||
|
componentClass: node,
|
||||||
|
node: templateUrlExpr,
|
||||||
|
template: templateStr,
|
||||||
|
templateUrl: resourceUrl,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {parseTemplate, templateSourceMapping};
|
||||||
|
}
|
||||||
|
|
||||||
|
private _extractInlineTemplate(
|
||||||
|
node: ClassDeclaration, decorator: Decorator, component: Map<string, ts.Expression>,
|
||||||
|
containingFile: string): {
|
||||||
|
parseTemplate: (options?: ParseTemplateOptions) => ParsedTemplate;
|
||||||
|
templateSourceMapping: TemplateSourceMapping
|
||||||
|
} {
|
||||||
if (!component.has('template')) {
|
if (!component.has('template')) {
|
||||||
return null;
|
throw new FatalDiagnosticError(
|
||||||
|
ErrorCode.COMPONENT_MISSING_TEMPLATE, decorator.node, 'component is missing a template');
|
||||||
}
|
}
|
||||||
const templateExpr = component.get('template') !;
|
const templateExpr = component.get('template') !;
|
||||||
|
|
||||||
let templateStr: string;
|
let templateStr: string;
|
||||||
let templateUrl: string = '';
|
let templateUrl: string = '';
|
||||||
let templateRange: LexerRange|undefined = undefined;
|
let templateRange: LexerRange|undefined = undefined;
|
||||||
|
let templateSourceMapping: TemplateSourceMapping;
|
||||||
let escapedString = false;
|
let escapedString = false;
|
||||||
// We only support SourceMaps for inline templates that are simple string literals.
|
// We only support SourceMaps for inline templates that are simple string literals.
|
||||||
if (ts.isStringLiteral(templateExpr) || ts.isNoSubstitutionTemplateLiteral(templateExpr)) {
|
if (ts.isStringLiteral(templateExpr) || ts.isNoSubstitutionTemplateLiteral(templateExpr)) {
|
||||||
|
@ -660,6 +639,10 @@ export class ComponentDecoratorHandler implements
|
||||||
templateStr = templateExpr.getSourceFile().text;
|
templateStr = templateExpr.getSourceFile().text;
|
||||||
templateUrl = containingFile;
|
templateUrl = containingFile;
|
||||||
escapedString = true;
|
escapedString = true;
|
||||||
|
templateSourceMapping = {
|
||||||
|
type: 'direct',
|
||||||
|
node: templateExpr as(ts.StringLiteral | ts.NoSubstitutionTemplateLiteral),
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
const resolvedTemplate = this.evaluator.evaluate(templateExpr);
|
const resolvedTemplate = this.evaluator.evaluate(templateExpr);
|
||||||
if (typeof resolvedTemplate !== 'string') {
|
if (typeof resolvedTemplate !== 'string') {
|
||||||
|
@ -667,8 +650,18 @@ export class ComponentDecoratorHandler implements
|
||||||
ErrorCode.VALUE_HAS_WRONG_TYPE, templateExpr, 'template must be a string');
|
ErrorCode.VALUE_HAS_WRONG_TYPE, templateExpr, 'template must be a string');
|
||||||
}
|
}
|
||||||
templateStr = resolvedTemplate;
|
templateStr = resolvedTemplate;
|
||||||
|
templateSourceMapping = {
|
||||||
|
type: 'indirect',
|
||||||
|
node: templateExpr,
|
||||||
|
componentClass: node,
|
||||||
|
template: templateStr,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return {templateStr, templateUrl, templateRange, escapedString};
|
|
||||||
|
const parseTemplate = (options?: ParseTemplateOptions) => this._parseTemplate(
|
||||||
|
component, templateStr, templateUrl, templateRange, escapedString, options);
|
||||||
|
|
||||||
|
return {parseTemplate, templateSourceMapping};
|
||||||
}
|
}
|
||||||
|
|
||||||
private _parseTemplate(
|
private _parseTemplate(
|
||||||
|
@ -826,4 +819,5 @@ export interface ParsedTemplate {
|
||||||
|
|
||||||
interface PreanalyzedTemplate extends ParsedTemplate {
|
interface PreanalyzedTemplate extends ParsedTemplate {
|
||||||
parseTemplate: (options?: ParseTemplateOptions) => ParsedTemplate;
|
parseTemplate: (options?: ParseTemplateOptions) => ParsedTemplate;
|
||||||
|
templateSourceMapping: TemplateSourceMapping;
|
||||||
}
|
}
|
||||||
|
|
|
@ -141,13 +141,6 @@ export function translateDiagnostic(
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let messageText: string;
|
|
||||||
if (typeof diagnostic.messageText === 'string') {
|
|
||||||
messageText = diagnostic.messageText;
|
|
||||||
} else {
|
|
||||||
messageText = diagnostic.messageText.messageText;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapping = resolver.getSourceMapping(sourceLocation.id);
|
const mapping = resolver.getSourceMapping(sourceLocation.id);
|
||||||
return makeTemplateDiagnostic(
|
return makeTemplateDiagnostic(
|
||||||
mapping, span, diagnostic.category, diagnostic.code, diagnostic.messageText);
|
mapping, span, diagnostic.category, diagnostic.code, diagnostic.messageText);
|
||||||
|
|
|
@ -283,7 +283,7 @@ export function performCompilation(
|
||||||
return {diagnostics: allDiagnostics, program};
|
return {diagnostics: allDiagnostics, program};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function defaultGatherDiagnostics(program: api.Program): Diagnostics {
|
export function defaultGatherDiagnostics(program: api.Program): Diagnostics {
|
||||||
const allDiagnostics: Array<ts.Diagnostic|api.Diagnostic> = [];
|
const allDiagnostics: Array<ts.Diagnostic|api.Diagnostic> = [];
|
||||||
|
|
||||||
function checkDiagnostics(diags: Diagnostics | undefined) {
|
function checkDiagnostics(diags: Diagnostics | undefined) {
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {CustomTransformers, Program} from '@angular/compiler-cli';
|
import {CustomTransformers, Program, defaultGatherDiagnostics} from '@angular/compiler-cli';
|
||||||
import * as api from '@angular/compiler-cli/src/transformers/api';
|
import * as api from '@angular/compiler-cli/src/transformers/api';
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
|
@ -100,6 +100,15 @@ export class NgtscTestEnvironment {
|
||||||
setWrapHostForTest(makeWrapHost(this.multiCompileHostExt));
|
setWrapHostForTest(makeWrapHost(this.multiCompileHostExt));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Installs a compiler host that allows for asynchronous reading of resources by implementing the
|
||||||
|
* `CompilerHost.readResource` method. Note that only asynchronous compilations are affected, as
|
||||||
|
* synchronous compilations do not use the asynchronous resource loader.
|
||||||
|
*/
|
||||||
|
enablePreloading(): void {
|
||||||
|
setWrapHostForTest(makeWrapHost(new ResourceLoadingCompileHost(this.fs)));
|
||||||
|
}
|
||||||
|
|
||||||
flushWrittenFileTracking(): void {
|
flushWrittenFileTracking(): void {
|
||||||
if (this.multiCompileHostExt === null) {
|
if (this.multiCompileHostExt === null) {
|
||||||
throw new Error(`Not tracking written files - call enableMultipleCompilations()`);
|
throw new Error(`Not tracking written files - call enableMultipleCompilations()`);
|
||||||
|
@ -192,6 +201,16 @@ export class NgtscTestEnvironment {
|
||||||
return mainDiagnosticsForTest(['-p', this.basePath]) as ts.Diagnostic[];
|
return mainDiagnosticsForTest(['-p', this.basePath]) as ts.Diagnostic[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async driveDiagnosticsAsync(): Promise<ReadonlyArray<ts.Diagnostic>> {
|
||||||
|
const {rootNames, options} = readNgcCommandLineAndConfiguration(['-p', this.basePath]);
|
||||||
|
const host = createCompilerHost({options});
|
||||||
|
const program = createProgram({rootNames, host, options});
|
||||||
|
await program.loadNgStructureAsync();
|
||||||
|
|
||||||
|
// ngtsc only produces ts.Diagnostic messages.
|
||||||
|
return defaultGatherDiagnostics(program as api.Program) as ts.Diagnostic[];
|
||||||
|
}
|
||||||
|
|
||||||
driveRoutes(entryPoint?: string): LazyRoute[] {
|
driveRoutes(entryPoint?: string): LazyRoute[] {
|
||||||
const {rootNames, options} = readNgcCommandLineAndConfiguration(['-p', this.basePath]);
|
const {rootNames, options} = readNgcCommandLineAndConfiguration(['-p', this.basePath]);
|
||||||
const host = createCompilerHost({options});
|
const host = createCompilerHost({options});
|
||||||
|
@ -251,6 +270,16 @@ class MultiCompileHostExt extends AugmentedCompilerHost implements Partial<ts.Co
|
||||||
invalidate(fileName: string): void { this.cache.delete(fileName); }
|
invalidate(fileName: string): void { this.cache.delete(fileName); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ResourceLoadingCompileHost extends AugmentedCompilerHost implements api.CompilerHost {
|
||||||
|
readResource(fileName: string): Promise<string>|string {
|
||||||
|
const resource = this.readFile(fileName);
|
||||||
|
if (resource === undefined) {
|
||||||
|
throw new Error(`Resource ${fileName} not found`);
|
||||||
|
}
|
||||||
|
return resource;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function makeWrapHost(wrapped: AugmentedCompilerHost): (host: ts.CompilerHost) => ts.CompilerHost {
|
function makeWrapHost(wrapped: AugmentedCompilerHost): (host: ts.CompilerHost) => ts.CompilerHost {
|
||||||
return (delegate) => {
|
return (delegate) => {
|
||||||
wrapped.delegate = delegate;
|
wrapped.delegate = delegate;
|
||||||
|
|
|
@ -469,9 +469,21 @@ export declare class CommonModule {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('error locations', () => {
|
// Test both sync and async compilations, see https://github.com/angular/angular/issues/32538
|
||||||
it('should be correct for direct templates', () => {
|
['sync', 'async'].forEach(mode => {
|
||||||
env.write('test.ts', `
|
describe(`error locations [${mode}]`, () => {
|
||||||
|
let driveDiagnostics: () => Promise<ReadonlyArray<ts.Diagnostic>>;
|
||||||
|
beforeEach(() => {
|
||||||
|
if (mode === 'async') {
|
||||||
|
env.enablePreloading();
|
||||||
|
driveDiagnostics = () => env.driveDiagnosticsAsync();
|
||||||
|
} else {
|
||||||
|
driveDiagnostics = () => Promise.resolve(env.driveDiagnostics());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be correct for direct templates', async() => {
|
||||||
|
env.write('test.ts', `
|
||||||
import {Component, NgModule} from '@angular/core';
|
import {Component, NgModule} from '@angular/core';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
@ -484,14 +496,14 @@ export declare class CommonModule {
|
||||||
user: {name: string}[];
|
user: {name: string}[];
|
||||||
}`);
|
}`);
|
||||||
|
|
||||||
const diags = env.driveDiagnostics();
|
const diags = await driveDiagnostics();
|
||||||
expect(diags.length).toBe(1);
|
expect(diags.length).toBe(1);
|
||||||
expect(diags[0].file !.fileName).toBe(_('/test.ts'));
|
expect(diags[0].file !.fileName).toBe(_('/test.ts'));
|
||||||
expect(getSourceCodeForDiagnostic(diags[0])).toBe('user.does_not_exist');
|
expect(getSourceCodeForDiagnostic(diags[0])).toBe('user.does_not_exist');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be correct for indirect templates', () => {
|
it('should be correct for indirect templates', async() => {
|
||||||
env.write('test.ts', `
|
env.write('test.ts', `
|
||||||
import {Component, NgModule} from '@angular/core';
|
import {Component, NgModule} from '@angular/core';
|
||||||
|
|
||||||
const TEMPLATE = \`<p>
|
const TEMPLATE = \`<p>
|
||||||
|
@ -506,18 +518,18 @@ export declare class CommonModule {
|
||||||
user: {name: string}[];
|
user: {name: string}[];
|
||||||
}`);
|
}`);
|
||||||
|
|
||||||
const diags = env.driveDiagnostics();
|
const diags = await driveDiagnostics();
|
||||||
expect(diags.length).toBe(1);
|
expect(diags.length).toBe(1);
|
||||||
expect(diags[0].file !.fileName).toBe(_('/test.ts') + ' (TestCmp template)');
|
expect(diags[0].file !.fileName).toBe(_('/test.ts') + ' (TestCmp template)');
|
||||||
expect(getSourceCodeForDiagnostic(diags[0])).toBe('user.does_not_exist');
|
expect(getSourceCodeForDiagnostic(diags[0])).toBe('user.does_not_exist');
|
||||||
expect(getSourceCodeForDiagnostic(diags[0].relatedInformation ![0])).toBe('TEMPLATE');
|
expect(getSourceCodeForDiagnostic(diags[0].relatedInformation ![0])).toBe('TEMPLATE');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be correct for external templates', () => {
|
it('should be correct for external templates', async() => {
|
||||||
env.write('template.html', `<p>
|
env.write('template.html', `<p>
|
||||||
{{user.does_not_exist}}
|
{{user.does_not_exist}}
|
||||||
</p>`);
|
</p>`);
|
||||||
env.write('test.ts', `
|
env.write('test.ts', `
|
||||||
import {Component, NgModule} from '@angular/core';
|
import {Component, NgModule} from '@angular/core';
|
||||||
|
|
||||||
|
|
||||||
|
@ -529,12 +541,13 @@ export declare class CommonModule {
|
||||||
user: {name: string}[];
|
user: {name: string}[];
|
||||||
}`);
|
}`);
|
||||||
|
|
||||||
const diags = env.driveDiagnostics();
|
const diags = await driveDiagnostics();
|
||||||
expect(diags.length).toBe(1);
|
expect(diags.length).toBe(1);
|
||||||
expect(diags[0].file !.fileName).toBe(_('/template.html'));
|
expect(diags[0].file !.fileName).toBe(_('/template.html'));
|
||||||
expect(getSourceCodeForDiagnostic(diags[0])).toBe('user.does_not_exist');
|
expect(getSourceCodeForDiagnostic(diags[0])).toBe('user.does_not_exist');
|
||||||
expect(getSourceCodeForDiagnostic(diags[0].relatedInformation ![0]))
|
expect(getSourceCodeForDiagnostic(diags[0].relatedInformation ![0]))
|
||||||
.toBe(`'./template.html'`);
|
.toBe(`'./template.html'`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue