refactor(compiler-cli): move template parse errors to TemplateData (#40026)

Durring analysis we find template parse errors. This commit changes
where the type checking context stores the parse errors. Previously, we
stored them on the AnalysisOutput this commit changes the errors to be
stored on the TemplateData (which is a property on the shim). That way,
the template parse errors can be grouped by template.

Previously, if a template had a parse error, we poisoned the module and
would not procede to find typecheck errors. This change does not poison
modules whose template have typecheck errors, so that ngtsc can emit
typecheck errors for templates with parse errors.

Additionally, all template diagnostics are produced in the same place.
This allows requesting just the template template diagnostics or just
other types of errors.

PR Close #40026
This commit is contained in:
Zach Arend 2020-12-03 11:42:46 -08:00 committed by Alex Rickabaugh
parent 9dedb62494
commit db97453ca0
8 changed files with 222 additions and 63 deletions

View File

@ -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 {compileComponentFromMetadata, compileDeclareComponentFromMetadata, ConstantPool, CssSelector, DEFAULT_INTERPOLATION_CONFIG, DomElementSchemaRegistry, Expression, ExternalExpr, Identifiers, InterpolationConfig, LexerRange, makeBindingParser, ParsedTemplate, ParseSourceFile, parseTemplate, R3ComponentDef, R3ComponentMetadata, R3FactoryTarget, R3TargetBinder, R3UsedDirectiveMetadata, SelectorMatcher, Statement, TmplAstNode, WrappedNodeExpr} from '@angular/compiler'; import {compileComponentFromMetadata, compileDeclareComponentFromMetadata, ConstantPool, CssSelector, DEFAULT_INTERPOLATION_CONFIG, DomElementSchemaRegistry, Expression, ExternalExpr, Identifiers, InterpolationConfig, LexerRange, makeBindingParser, ParsedTemplate, ParseSourceFile, parseTemplate, R3ComponentDef, R3ComponentMetadata, R3FactoryTarget, R3TargetBinder, R3UsedDirectiveMetadata, SelectorMatcher, Statement, syntaxError, TmplAstNode, WrappedNodeExpr} from '@angular/compiler';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {CycleAnalyzer} from '../../cycles'; import {CycleAnalyzer} from '../../cycles';
@ -266,28 +266,6 @@ export class ComponentDecoratorHandler implements
{path: null, expression: component.get('template')!} : {path: null, expression: component.get('template')!} :
{path: absoluteFrom(template.templateUrl), expression: template.sourceMapping.node}; {path: absoluteFrom(template.templateUrl), expression: template.sourceMapping.node};
let diagnostics: ts.Diagnostic[]|undefined = undefined;
if (template.errors !== null) {
// If there are any template parsing errors, convert them to `ts.Diagnostic`s for display.
const id = getTemplateId(node);
diagnostics = template.errors.map(error => {
const span = error.span;
if (span.start.offset === span.end.offset) {
// Template errors can contain zero-length spans, if the error occurs at a single point.
// However, TypeScript does not handle displaying a zero-length diagnostic very well, so
// increase the ending offset by 1 for such errors, to ensure the position is shown in the
// diagnostic.
span.end.offset++;
}
return makeTemplateDiagnostic(
id, template.sourceMapping, span, ts.DiagnosticCategory.Error,
ngErrorCode(ErrorCode.TEMPLATE_PARSE_ERROR), error.msg);
});
}
// Figure out the set of styles. The ordering here is important: external resources (styleUrls) // Figure out the set of styles. The ordering here is important: external resources (styleUrls)
// precede inline styles, and styles defined in the template override styles defined in the // precede inline styles, and styles defined in the template override styles defined in the
// component. // component.
@ -370,9 +348,8 @@ export class ComponentDecoratorHandler implements
styles: styleResources, styles: styleResources,
template: templateResource, template: templateResource,
}, },
isPoisoned: diagnostics !== undefined && diagnostics.length > 0, isPoisoned: false,
}, },
diagnostics,
}; };
if (changeDetection !== null) { if (changeDetection !== null) {
output.analysis!.meta.changeDetection = changeDetection; output.analysis!.meta.changeDetection = changeDetection;
@ -456,7 +433,7 @@ export class ComponentDecoratorHandler implements
const binder = new R3TargetBinder(scope.matcher); const binder = new R3TargetBinder(scope.matcher);
ctx.addTemplate( ctx.addTemplate(
new Reference(node), binder, meta.template.diagNodes, scope.pipes, scope.schemas, new Reference(node), binder, meta.template.diagNodes, scope.pipes, scope.schemas,
meta.template.sourceMapping, meta.template.file); meta.template.sourceMapping, meta.template.file, meta.template.errors);
} }
resolve(node: ClassDeclaration, analysis: Readonly<ComponentAnalysisData>): resolve(node: ClassDeclaration, analysis: Readonly<ComponentAnalysisData>):
@ -616,6 +593,9 @@ export class ComponentDecoratorHandler implements
compileFull( compileFull(
node: ClassDeclaration, analysis: Readonly<ComponentAnalysisData>, node: ClassDeclaration, analysis: Readonly<ComponentAnalysisData>,
resolution: Readonly<ComponentResolutionData>, pool: ConstantPool): CompileResult[] { resolution: Readonly<ComponentResolutionData>, pool: ConstantPool): CompileResult[] {
if (analysis.template.errors !== null && analysis.template.errors.length > 0) {
return [];
}
const meta: R3ComponentMetadata = {...analysis.meta, ...resolution}; const meta: R3ComponentMetadata = {...analysis.meta, ...resolution};
const def = compileComponentFromMetadata(meta, pool, makeBindingParser()); const def = compileComponentFromMetadata(meta, pool, makeBindingParser());
return this.compileComponent(analysis, def); return this.compileComponent(analysis, def);
@ -624,6 +604,9 @@ export class ComponentDecoratorHandler implements
compilePartial( compilePartial(
node: ClassDeclaration, analysis: Readonly<ComponentAnalysisData>, node: ClassDeclaration, analysis: Readonly<ComponentAnalysisData>,
resolution: Readonly<ComponentResolutionData>): CompileResult[] { resolution: Readonly<ComponentResolutionData>): CompileResult[] {
if (analysis.template.errors !== null && analysis.template.errors.length > 0) {
return [];
}
const meta: R3ComponentMetadata = {...analysis.meta, ...resolution}; const meta: R3ComponentMetadata = {...analysis.meta, ...resolution};
const def = compileDeclareComponentFromMetadata(meta, analysis.template); const def = compileDeclareComponentFromMetadata(meta, analysis.template);
return this.compileComponent(analysis, def); return this.compileComponent(analysis, def);

View File

@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {ConstantPool} from '@angular/compiler';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {CycleAnalyzer, ImportGraph} from '../../cycles'; import {CycleAnalyzer, ImportGraph} from '../../cycles';
@ -219,6 +220,38 @@ runInEachFileSystem(() => {
const {analysis} = handler.analyze(TestCmp, detected.metadata); const {analysis} = handler.analyze(TestCmp, detected.metadata);
expect(analysis?.resources.styles.size).toBe(3); expect(analysis?.resources.styles.size).toBe(3);
}); });
it('does not emit a program with template parse errors', () => {
const template = '{{x ? y }}';
const {program, options, host} = makeProgram([
{
name: _('/node_modules/@angular/core/index.d.ts'),
contents: 'export const Component: any;',
},
{
name: _('/entry.ts'),
contents: `
import {Component} from '@angular/core';
@Component({
template: '${template}',
}) class TestCmp {}
`
},
]);
const {reflectionHost, handler} = setup(program, options, host);
const TestCmp = getDeclaration(program, _('/entry.ts'), 'TestCmp', isNamedClassDeclaration);
const detected = handler.detect(TestCmp, reflectionHost.getDecoratorsOfDeclaration(TestCmp));
if (detected === undefined) {
return fail('Failed to recognize @Component');
}
const {analysis} = handler.analyze(TestCmp, detected.metadata);
const resolution = handler.resolve(TestCmp, analysis!);
const compileResult =
handler.compileFull(TestCmp, analysis!, resolution.data!, new ConstantPool());
expect(compileResult).toEqual([]);
});
}); });
function ivyCode(code: ErrorCode): number { function ivyCode(code: ErrorCode): number {

View File

@ -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 {ParseSourceFile, R3TargetBinder, SchemaMetadata, TmplAstNode} from '@angular/compiler'; import {ParseError, ParseSourceFile, R3TargetBinder, SchemaMetadata, TmplAstNode} from '@angular/compiler';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {Reference} from '../../imports'; import {Reference} from '../../imports';
@ -35,12 +35,14 @@ export interface TypeCheckContext {
* @param sourceMapping a `TemplateSourceMapping` instance which describes the origin of the * @param sourceMapping a `TemplateSourceMapping` instance which describes the origin of the
* template text described by the AST. * template text described by the AST.
* @param file the `ParseSourceFile` associated with the template. * @param file the `ParseSourceFile` associated with the template.
* @param parseErrors the `ParseError`'s associated with the template.
*/ */
addTemplate( addTemplate(
ref: Reference<ClassDeclaration<ts.ClassDeclaration>>, ref: Reference<ClassDeclaration<ts.ClassDeclaration>>,
binder: R3TargetBinder<TypeCheckableDirectiveMeta>, template: TmplAstNode[], binder: R3TargetBinder<TypeCheckableDirectiveMeta>, template: TmplAstNode[],
pipes: Map<string, Reference<ClassDeclaration<ts.ClassDeclaration>>>, pipes: Map<string, Reference<ClassDeclaration<ts.ClassDeclaration>>>,
schemas: SchemaMetadata[], sourceMapping: TemplateSourceMapping, file: ParseSourceFile): void; schemas: SchemaMetadata[], sourceMapping: TemplateSourceMapping, file: ParseSourceFile,
parseErrors: ParseError[]|null): void;
} }
/** /**

View File

@ -20,7 +20,7 @@ import {DirectiveInScope, ElementSymbol, FullTemplateMapping, GlobalCompletion,
import {TemplateDiagnostic} from '../diagnostics'; import {TemplateDiagnostic} from '../diagnostics';
import {CompletionEngine} from './completion'; import {CompletionEngine} from './completion';
import {InliningMode, ShimTypeCheckingData, TemplateData, TypeCheckContextImpl, TypeCheckingHost} from './context'; import {InliningMode, ShimTypeCheckingData, TemplateData, TemplateOverride, TypeCheckContextImpl, TypeCheckingHost} from './context';
import {shouldReportDiagnostic, translateDiagnostic} from './diagnostics'; import {shouldReportDiagnostic, translateDiagnostic} from './diagnostics';
import {TemplateSourceManager} from './source'; import {TemplateSourceManager} from './source';
import {findTypeCheckBlock, getTemplateMapping, TemplateSourceResolver} from './tcb_util'; import {findTypeCheckBlock, getTemplateMapping, TemplateSourceResolver} from './tcb_util';
@ -167,7 +167,7 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
fileRecord.templateOverrides = new Map(); fileRecord.templateOverrides = new Map();
} }
fileRecord.templateOverrides.set(id, nodes); fileRecord.templateOverrides.set(id, {nodes, errors});
// Clear data for the shim in question, so it'll be regenerated on the next request. // Clear data for the shim in question, so it'll be regenerated on the next request.
const shimFile = this.typeCheckingStrategy.shimPathForComponent(component); const shimFile = this.typeCheckingStrategy.shimPathForComponent(component);
@ -217,8 +217,8 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
} }
/** /**
* Retrieve type-checking diagnostics from the given `ts.SourceFile` using the most recent * Retrieve type-checking and template parse diagnostics from the given `ts.SourceFile` using the
* type-checking program. * most recent type-checking program.
*/ */
getDiagnosticsForFile(sf: ts.SourceFile, optimizeFor: OptimizeFor): ts.Diagnostic[] { getDiagnosticsForFile(sf: ts.SourceFile, optimizeFor: OptimizeFor): ts.Diagnostic[] {
switch (optimizeFor) { switch (optimizeFor) {
@ -247,6 +247,10 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
diagnostics.push(...typeCheckProgram.getSemanticDiagnostics(shimSf).map( diagnostics.push(...typeCheckProgram.getSemanticDiagnostics(shimSf).map(
diag => convertDiagnostic(diag, fileRecord.sourceManager))); diag => convertDiagnostic(diag, fileRecord.sourceManager)));
diagnostics.push(...shimRecord.genesisDiagnostics); diagnostics.push(...shimRecord.genesisDiagnostics);
for (const templateData of shimRecord.templates.values()) {
diagnostics.push(...templateData.templateDiagnostics);
}
} }
return diagnostics.filter((diag: ts.Diagnostic|null): diag is ts.Diagnostic => diag !== null); return diagnostics.filter((diag: ts.Diagnostic|null): diag is ts.Diagnostic => diag !== null);
@ -282,6 +286,10 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
diag => convertDiagnostic(diag, fileRecord.sourceManager))); diag => convertDiagnostic(diag, fileRecord.sourceManager)));
diagnostics.push(...shimRecord.genesisDiagnostics); diagnostics.push(...shimRecord.genesisDiagnostics);
for (const templateData of shimRecord.templates.values()) {
diagnostics.push(...templateData.templateDiagnostics);
}
return diagnostics.filter( return diagnostics.filter(
(diag: TemplateDiagnostic|null): diag is TemplateDiagnostic => (diag: TemplateDiagnostic|null): diag is TemplateDiagnostic =>
diag !== null && diag.templateId === templateId); diag !== null && diag.templateId === templateId);
@ -650,7 +658,7 @@ export interface FileTypeCheckingData {
/** /**
* Map of template overrides applied to any components in this input file. * Map of template overrides applied to any components in this input file.
*/ */
templateOverrides: Map<TemplateId, TmplAstNode[]>|null; templateOverrides: Map<TemplateId, TemplateOverride>|null;
/** /**
* Data for each shim generated from this input file. * Data for each shim generated from this input file.
@ -684,7 +692,7 @@ class WholeProgramTypeCheckingHost implements TypeCheckingHost {
return !fileData.shimData.has(shimPath); return !fileData.shimData.has(shimPath);
} }
getTemplateOverride(sfPath: AbsoluteFsPath, node: ts.ClassDeclaration): TmplAstNode[]|null { getTemplateOverride(sfPath: AbsoluteFsPath, node: ts.ClassDeclaration): TemplateOverride|null {
const fileData = this.impl.getFileData(sfPath); const fileData = this.impl.getFileData(sfPath);
if (fileData.templateOverrides === null) { if (fileData.templateOverrides === null) {
return null; return null;
@ -742,7 +750,7 @@ class SingleFileTypeCheckingHost implements TypeCheckingHost {
return !this.fileData.shimData.has(shimPath); return !this.fileData.shimData.has(shimPath);
} }
getTemplateOverride(sfPath: AbsoluteFsPath, node: ts.ClassDeclaration): TmplAstNode[]|null { getTemplateOverride(sfPath: AbsoluteFsPath, node: ts.ClassDeclaration): TemplateOverride|null {
this.assertPath(sfPath); this.assertPath(sfPath);
if (this.fileData.templateOverrides === null) { if (this.fileData.templateOverrides === null) {
return null; return null;

View File

@ -6,7 +6,8 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {BoundTarget, ParseSourceFile, R3TargetBinder, SchemaMetadata, TmplAstNode} from '@angular/compiler'; import {BoundTarget, ParseError, ParseSourceFile, R3TargetBinder, SchemaMetadata, TemplateParseError, TmplAstNode} from '@angular/compiler';
import {ErrorCode, ngErrorCode} from '@angular/compiler-cli/src/ngtsc/diagnostics';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {absoluteFromSourceFile, AbsoluteFsPath} from '../../file_system'; import {absoluteFromSourceFile, AbsoluteFsPath} from '../../file_system';
@ -14,7 +15,7 @@ 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, TemplateId, TemplateSourceMapping, TypeCheckableDirectiveMeta, TypeCheckBlockMetadata, TypeCheckContext, TypeCheckingConfig, TypeCtorMetadata} from '../api'; import {ComponentToShimMappingStrategy, TemplateId, TemplateSourceMapping, TypeCheckableDirectiveMeta, TypeCheckBlockMetadata, TypeCheckContext, TypeCheckingConfig, TypeCtorMetadata} from '../api';
import {TemplateDiagnostic} from '../diagnostics'; import {makeTemplateDiagnostic, TemplateDiagnostic} from '../diagnostics';
import {DomSchemaChecker, RegistryDomSchemaChecker} from './dom'; import {DomSchemaChecker, RegistryDomSchemaChecker} from './dom';
import {Environment} from './environment'; import {Environment} from './environment';
@ -50,6 +51,11 @@ export interface ShimTypeCheckingData {
templates: Map<TemplateId, TemplateData>; templates: Map<TemplateId, TemplateData>;
} }
export interface TemplateOverride {
nodes: TmplAstNode[];
errors: ParseError[]|null;
}
/** /**
* Data tracked for each template processed by the template type-checking system. * Data tracked for each template processed by the template type-checking system.
*/ */
@ -64,6 +70,11 @@ export interface TemplateData {
* template nodes. * template nodes.
*/ */
boundTarget: BoundTarget<TypeCheckableDirectiveMeta>; boundTarget: BoundTarget<TypeCheckableDirectiveMeta>;
/**
* Errors found while parsing them template, which have been converted to diagnostics.
*/
templateDiagnostics: TemplateDiagnostic[];
} }
/** /**
@ -136,7 +147,7 @@ export interface TypeCheckingHost {
* Check if the given component has had its template overridden, and retrieve the new template * Check if the given component has had its template overridden, and retrieve the new template
* nodes if so. * nodes if so.
*/ */
getTemplateOverride(sfPath: AbsoluteFsPath, node: ts.ClassDeclaration): TmplAstNode[]|null; getTemplateOverride(sfPath: AbsoluteFsPath, node: ts.ClassDeclaration): TemplateOverride|null;
/** /**
* Report data from a shim generated from the given input file path. * Report data from a shim generated from the given input file path.
@ -194,35 +205,42 @@ export class TypeCheckContextImpl implements TypeCheckContext {
private typeCtorPending = new Set<ts.ClassDeclaration>(); private typeCtorPending = new Set<ts.ClassDeclaration>();
/** /**
* Record a template for the given component `node`, with a `SelectorMatcher` for directive * Register a template to potentially be type-checked.
* matching.
* *
* @param node class of the node being recorded. * Implements `TypeCheckContext.addTemplate`.
* @param template AST nodes of the template being recorded.
* @param matcher `SelectorMatcher` which tracks directives that are in scope for this template.
*/ */
addTemplate( addTemplate(
ref: Reference<ClassDeclaration<ts.ClassDeclaration>>, ref: Reference<ClassDeclaration<ts.ClassDeclaration>>,
binder: R3TargetBinder<TypeCheckableDirectiveMeta>, template: TmplAstNode[], binder: R3TargetBinder<TypeCheckableDirectiveMeta>, template: TmplAstNode[],
pipes: Map<string, Reference<ClassDeclaration<ts.ClassDeclaration>>>, pipes: Map<string, Reference<ClassDeclaration<ts.ClassDeclaration>>>,
schemas: SchemaMetadata[], sourceMapping: TemplateSourceMapping, schemas: SchemaMetadata[], sourceMapping: TemplateSourceMapping, file: ParseSourceFile,
file: ParseSourceFile): void { parseErrors: ParseError[]|null): void {
if (!this.host.shouldCheckComponent(ref.node)) { if (!this.host.shouldCheckComponent(ref.node)) {
return; return;
} }
const fileData = this.dataForFile(ref.node.getSourceFile());
const shimData = this.pendingShimForComponent(ref.node);
const templateId = fileData.sourceManager.getTemplateId(ref.node);
const templateDiagnostics: TemplateDiagnostic[] = [];
const sfPath = absoluteFromSourceFile(ref.node.getSourceFile()); const sfPath = absoluteFromSourceFile(ref.node.getSourceFile());
const overrideTemplate = this.host.getTemplateOverride(sfPath, ref.node); const overrideTemplate = this.host.getTemplateOverride(sfPath, ref.node);
if (overrideTemplate !== null) { if (overrideTemplate !== null) {
template = overrideTemplate; template = overrideTemplate.nodes;
parseErrors = overrideTemplate.errors;
}
if (parseErrors !== null) {
templateDiagnostics.push(
...this.getTemplateDiagnostics(parseErrors, templateId, sourceMapping));
} }
// Accumulate a list of any directives which could not have type constructors generated due to // Accumulate a list of any directives which could not have type constructors generated due to
// unsupported inlining operations. // unsupported inlining operations.
let missingInlines: ClassDeclaration[] = []; let missingInlines: ClassDeclaration[] = [];
const fileData = this.dataForFile(ref.node.getSourceFile());
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.
@ -251,10 +269,11 @@ export class TypeCheckContextImpl implements TypeCheckContext {
}); });
} }
} }
const templateId = fileData.sourceManager.getTemplateId(ref.node);
shimData.templates.set(templateId, { shimData.templates.set(templateId, {
template, template,
boundTarget, boundTarget,
templateDiagnostics,
}); });
const tcbRequiresInline = requiresInlineTypeCheckBlock(ref.node, pipes); const tcbRequiresInline = requiresInlineTypeCheckBlock(ref.node, pipes);
@ -435,6 +454,26 @@ export class TypeCheckContextImpl implements TypeCheckContext {
return this.fileMap.get(sfPath)!; return this.fileMap.get(sfPath)!;
} }
private getTemplateDiagnostics(
parseErrors: ParseError[], templateId: TemplateId,
sourceMapping: TemplateSourceMapping): TemplateDiagnostic[] {
return parseErrors.map(error => {
const span = error.span;
if (span.start.offset === span.end.offset) {
// Template errors can contain zero-length spans, if the error occurs at a single point.
// However, TypeScript does not handle displaying a zero-length diagnostic very well, so
// increase the ending offset by 1 for such errors, to ensure the position is shown in the
// diagnostic.
span.end.offset++;
}
return makeTemplateDiagnostic(
templateId, sourceMapping, span, ts.DiagnosticCategory.Error,
ngErrorCode(ErrorCode.TEMPLATE_PARSE_ERROR), error.msg);
});
}
} }
/** /**

View File

@ -407,7 +407,7 @@ export function setup(targets: TypeCheckingTarget[], overrides: {
node: classRef.node.name, node: classRef.node.name,
}; };
ctx.addTemplate(classRef, binder, nodes, pipes, [], sourceMapping, templateFile); ctx.addTemplate(classRef, binder, nodes, pipes, [], sourceMapping, templateFile, errors);
} }
} }
}); });

View File

@ -7577,10 +7577,13 @@ export const Foo = Foo__PRE_R3__;
env.write('test.ts', ` env.write('test.ts', `
import {Component} from '@angular/core'; import {Component} from '@angular/core';
@Component({ @Component({
template: '<cmp [input]="x ? y">', template: '<input [value]="x ? y"/>',
selector: 'test-cmp', selector: 'test-cmp',
}) })
export class TestCmp {} export class TestCmp {
x = null;
y = null;
}
`); `);
const diags = env.driveDiagnostics(); const diags = env.driveDiagnostics();
expect(diags.length).toBe(1); expect(diags.length).toBe(1);
@ -7591,19 +7594,41 @@ export const Foo = Foo__PRE_R3__;
env.write('test.ts', ` env.write('test.ts', `
import {Component} from '@angular/core'; import {Component} from '@angular/core';
@Component({ @Component({
template: '<cmp [input]="x>', template: '<input [value]="x/>',
selector: 'test-cmp', selector: 'test-cmp',
}) })
export class TestCmp {} export class TestCmp {
x = null;
}
`); `);
const diags = env.driveDiagnostics(); const diags = env.driveDiagnostics();
expect(diags.length).toBe(1); expect(diags.length).toBe(1);
expect(getDiagnosticSourceCode(diags[0])).toBe('\''); expect(getDiagnosticSourceCode(diags[0])).toBe('\'');
}); });
it('should emit both type-check diagnostics and parse error diagnostics', () => {
env.write('test.ts', `
import {Component} from '@angular/core';
@Component({
template: \`<input (click)="x = 'invalid'"/> {{x = 2}}\`,
selector: 'test-cmp',
})
export class TestCmp {
x: number = 1;
}
`);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(2);
expect(diags[0].messageText).toEqual(`Type 'string' is not assignable to type 'number'.`);
expect(diags[1].messageText)
.toContain(
'Parser Error: Bindings cannot contain assignments at column 5 in [ {{x = 2}}]');
});
}); });
describe('i18n errors', () => { describe('i18n errors', () => {
it('should report helpful error message on nested i18n sections', () => { it('reports a diagnostics on nested i18n sections', () => {
env.write('test.ts', ` env.write('test.ts', `
import {Component} from '@angular/core'; import {Component} from '@angular/core';
@Component({ @Component({
@ -7625,7 +7650,7 @@ export const Foo = Foo__PRE_R3__;
.toEqual('<div i18n>Content</div>'); .toEqual('<div i18n>Content</div>');
}); });
it('report a diagnostic on nested i18n sections with tags in between', () => { it('reports a diagnostic on nested i18n sections with tags in between', () => {
env.write('test.ts', ` env.write('test.ts', `
import {Component} from '@angular/core'; import {Component} from '@angular/core';
@Component({ @Component({
@ -7647,7 +7672,7 @@ export const Foo = Foo__PRE_R3__;
.toEqual('<div i18n>Content</div>'); .toEqual('<div i18n>Content</div>');
}); });
it('report a diagnostic on nested i18n sections represented with <ng-continers>s', () => { it('reports a diagnostic on nested i18n sections represented with <ng-continers>s', () => {
env.write('test.ts', ` env.write('test.ts', `
import {Component} from '@angular/core'; import {Component} from '@angular/core';
@Component({ @Component({

View File

@ -13,8 +13,6 @@ import * as ts from 'typescript';
import {createModuleWithDeclarations} from './test_utils'; import {createModuleWithDeclarations} from './test_utils';
describe('getSemanticDiagnostics', () => { describe('getSemanticDiagnostics', () => {
let env: LanguageServiceTestEnvironment;
beforeEach(() => { beforeEach(() => {
initMockFileSystem('Native'); initMockFileSystem('Native');
}); });
@ -31,7 +29,7 @@ describe('getSemanticDiagnostics', () => {
export class AppComponent {} export class AppComponent {}
` `
}; };
env = createModuleWithDeclarations([appFile]); const env = createModuleWithDeclarations([appFile]);
const diags = env.ngLS.getSemanticDiagnostics(absoluteFrom('/app.ts')); const diags = env.ngLS.getSemanticDiagnostics(absoluteFrom('/app.ts'));
expect(diags.length).toEqual(0); expect(diags.length).toEqual(0);
@ -49,7 +47,7 @@ describe('getSemanticDiagnostics', () => {
export class AppComponent {} export class AppComponent {}
` `
}; };
env = createModuleWithDeclarations([appFile]); const env = createModuleWithDeclarations([appFile]);
const diags = env.ngLS.getSemanticDiagnostics(absoluteFrom('/app.ts')); const diags = env.ngLS.getSemanticDiagnostics(absoluteFrom('/app.ts'));
expect(diags.length).toBe(1); expect(diags.length).toBe(1);
@ -78,7 +76,7 @@ describe('getSemanticDiagnostics', () => {
` `
}; };
env = createModuleWithDeclarations([appFile], [templateFile]); const env = createModuleWithDeclarations([appFile], [templateFile]);
const diags = env.ngLS.getSemanticDiagnostics(absoluteFrom('/app.html')); const diags = env.ngLS.getSemanticDiagnostics(absoluteFrom('/app.html'));
expect(diags).toEqual([]); expect(diags).toEqual([]);
}); });
@ -97,7 +95,7 @@ describe('getSemanticDiagnostics', () => {
}; };
const templateFile = {name: absoluteFrom('/app.html'), contents: `{{nope}}`}; const templateFile = {name: absoluteFrom('/app.html'), contents: `{{nope}}`};
env = createModuleWithDeclarations([appFile], [templateFile]); const env = createModuleWithDeclarations([appFile], [templateFile]);
const diags = env.ngLS.getSemanticDiagnostics(absoluteFrom('/app.html')); const diags = env.ngLS.getSemanticDiagnostics(absoluteFrom('/app.html'));
expect(diags.length).toBe(1); expect(diags.length).toBe(1);
const {category, file, start, length, messageText} = diags[0]; const {category, file, start, length, messageText} = diags[0];
@ -105,4 +103,75 @@ describe('getSemanticDiagnostics', () => {
expect(file?.fileName).toBe('/app.html'); expect(file?.fileName).toBe('/app.html');
expect(messageText).toBe(`Property 'nope' does not exist on type 'AppComponent'.`); expect(messageText).toBe(`Property 'nope' does not exist on type 'AppComponent'.`);
}); });
it('should report a parse error in external template', () => {
const appFile = {
name: absoluteFrom('/app.ts'),
contents: `
import {Component, NgModule} from '@angular/core';
@Component({
templateUrl: './app.html'
})
export class AppComponent {
nope = false;
}
`
};
const templateFile = {name: absoluteFrom('/app.html'), contents: `{{nope = true}}`};
const env = createModuleWithDeclarations([appFile], [templateFile]);
const diags = env.ngLS.getSemanticDiagnostics(absoluteFrom('/app.html'));
expect(diags.length).toBe(1);
const {category, file, messageText} = diags[0];
expect(category).toBe(ts.DiagnosticCategory.Error);
expect(file?.fileName).toBe('/app.html');
expect(messageText)
.toContain(
`Parser Error: Bindings cannot contain assignments at column 8 in [{{nope = true}}]`);
});
it('should report parse errors of components defined in the same ts file', () => {
const appFile = {
name: absoluteFrom('/app.ts'),
contents: `
import {Component, NgModule} from '@angular/core';
@Component({ templateUrl: './app1.html' })
export class AppComponent1 { nope = false; }
@Component({ templateUrl: './app2.html' })
export class AppComponent2 { nope = false; }
`
};
const templateFile1 = {name: absoluteFrom('/app1.html'), contents: `{{nope = false}}`};
const templateFile2 = {name: absoluteFrom('/app2.html'), contents: `{{nope = true}}`};
const moduleFile = {
name: absoluteFrom('/app-module.ts'),
contents: `
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {AppComponent, AppComponent2} from './app';
@NgModule({
declarations: [AppComponent, AppComponent2],
imports: [CommonModule],
})
export class AppModule {}
`,
isRoot: true
};
const env =
LanguageServiceTestEnvironment.setup([moduleFile, appFile, templateFile1, templateFile2]);
const diags = env.ngLS.getSemanticDiagnostics(absoluteFrom('/app.ts'));
expect(diags.map(x => x.messageText).sort()).toEqual([
'Parser Error: Bindings cannot contain assignments at column 8 in [{{nope = false}}] in /app1.html@0:0',
'Parser Error: Bindings cannot contain assignments at column 8 in [{{nope = true}}] in /app2.html@0:0'
]);
});
}); });