diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts index 7ec79625eb..e4d39317b6 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts @@ -6,7 +6,7 @@ * 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 {CycleAnalyzer} from '../../cycles'; @@ -266,28 +266,6 @@ export class ComponentDecoratorHandler implements {path: null, expression: component.get('template')!} : {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) // precede inline styles, and styles defined in the template override styles defined in the // component. @@ -370,9 +348,8 @@ export class ComponentDecoratorHandler implements styles: styleResources, template: templateResource, }, - isPoisoned: diagnostics !== undefined && diagnostics.length > 0, + isPoisoned: false, }, - diagnostics, }; if (changeDetection !== null) { output.analysis!.meta.changeDetection = changeDetection; @@ -456,7 +433,7 @@ export class ComponentDecoratorHandler implements const binder = new R3TargetBinder(scope.matcher); ctx.addTemplate( 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): @@ -616,6 +593,9 @@ export class ComponentDecoratorHandler implements compileFull( node: ClassDeclaration, analysis: Readonly, resolution: Readonly, pool: ConstantPool): CompileResult[] { + if (analysis.template.errors !== null && analysis.template.errors.length > 0) { + return []; + } const meta: R3ComponentMetadata = {...analysis.meta, ...resolution}; const def = compileComponentFromMetadata(meta, pool, makeBindingParser()); return this.compileComponent(analysis, def); @@ -624,6 +604,9 @@ export class ComponentDecoratorHandler implements compilePartial( node: ClassDeclaration, analysis: Readonly, resolution: Readonly): CompileResult[] { + if (analysis.template.errors !== null && analysis.template.errors.length > 0) { + return []; + } const meta: R3ComponentMetadata = {...analysis.meta, ...resolution}; const def = compileDeclareComponentFromMetadata(meta, analysis.template); return this.compileComponent(analysis, def); diff --git a/packages/compiler-cli/src/ngtsc/annotations/test/component_spec.ts b/packages/compiler-cli/src/ngtsc/annotations/test/component_spec.ts index 51d139cee7..eff64819b0 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/test/component_spec.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/test/component_spec.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ +import {ConstantPool} from '@angular/compiler'; import * as ts from 'typescript'; import {CycleAnalyzer, ImportGraph} from '../../cycles'; @@ -219,6 +220,38 @@ runInEachFileSystem(() => { const {analysis} = handler.analyze(TestCmp, detected.metadata); 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 { diff --git a/packages/compiler-cli/src/ngtsc/typecheck/api/context.ts b/packages/compiler-cli/src/ngtsc/typecheck/api/context.ts index 9895075cd4..e738aa46b3 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/api/context.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/api/context.ts @@ -6,7 +6,7 @@ * 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 {Reference} from '../../imports'; @@ -35,12 +35,14 @@ export interface TypeCheckContext { * @param sourceMapping a `TemplateSourceMapping` instance which describes the origin of the * template text described by the AST. * @param file the `ParseSourceFile` associated with the template. + * @param parseErrors the `ParseError`'s associated with the template. */ addTemplate( ref: Reference>, binder: R3TargetBinder, template: TmplAstNode[], pipes: Map>>, - schemas: SchemaMetadata[], sourceMapping: TemplateSourceMapping, file: ParseSourceFile): void; + schemas: SchemaMetadata[], sourceMapping: TemplateSourceMapping, file: ParseSourceFile, + parseErrors: ParseError[]|null): void; } /** diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts index 0cd7b9de3b..2cd5cfffa4 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts @@ -20,7 +20,7 @@ import {DirectiveInScope, ElementSymbol, FullTemplateMapping, GlobalCompletion, import {TemplateDiagnostic} from '../diagnostics'; 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 {TemplateSourceManager} from './source'; import {findTypeCheckBlock, getTemplateMapping, TemplateSourceResolver} from './tcb_util'; @@ -167,7 +167,7 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { 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. 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 - * type-checking program. + * Retrieve type-checking and template parse diagnostics from the given `ts.SourceFile` using the + * most recent type-checking program. */ getDiagnosticsForFile(sf: ts.SourceFile, optimizeFor: OptimizeFor): ts.Diagnostic[] { switch (optimizeFor) { @@ -247,6 +247,10 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { diagnostics.push(...typeCheckProgram.getSemanticDiagnostics(shimSf).map( diag => convertDiagnostic(diag, fileRecord.sourceManager))); 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); @@ -282,6 +286,10 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { diag => convertDiagnostic(diag, fileRecord.sourceManager))); diagnostics.push(...shimRecord.genesisDiagnostics); + for (const templateData of shimRecord.templates.values()) { + diagnostics.push(...templateData.templateDiagnostics); + } + return diagnostics.filter( (diag: TemplateDiagnostic|null): diag is TemplateDiagnostic => diag !== null && diag.templateId === templateId); @@ -650,7 +658,7 @@ export interface FileTypeCheckingData { /** * Map of template overrides applied to any components in this input file. */ - templateOverrides: Map|null; + templateOverrides: Map|null; /** * Data for each shim generated from this input file. @@ -684,7 +692,7 @@ class WholeProgramTypeCheckingHost implements TypeCheckingHost { 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); if (fileData.templateOverrides === null) { return null; @@ -742,7 +750,7 @@ class SingleFileTypeCheckingHost implements TypeCheckingHost { 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); if (this.fileData.templateOverrides === null) { return null; diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts index c9c0cc7723..1bb8845a0f 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts @@ -6,7 +6,8 @@ * 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 {absoluteFromSourceFile, AbsoluteFsPath} from '../../file_system'; @@ -14,7 +15,7 @@ import {NoopImportRewriter, Reference, ReferenceEmitter} from '../../imports'; import {ClassDeclaration, ReflectionHost} from '../../reflection'; import {ImportManager} from '../../translator'; 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 {Environment} from './environment'; @@ -50,6 +51,11 @@ export interface ShimTypeCheckingData { templates: Map; } +export interface TemplateOverride { + nodes: TmplAstNode[]; + errors: ParseError[]|null; +} + /** * Data tracked for each template processed by the template type-checking system. */ @@ -64,6 +70,11 @@ export interface TemplateData { * template nodes. */ boundTarget: BoundTarget; + + /** + * 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 * 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. @@ -194,35 +205,42 @@ export class TypeCheckContextImpl implements TypeCheckContext { private typeCtorPending = new Set(); /** - * Record a template for the given component `node`, with a `SelectorMatcher` for directive - * matching. + * Register a template to potentially be type-checked. * - * @param node class of the node being recorded. - * @param template AST nodes of the template being recorded. - * @param matcher `SelectorMatcher` which tracks directives that are in scope for this template. + * Implements `TypeCheckContext.addTemplate`. */ addTemplate( ref: Reference>, binder: R3TargetBinder, template: TmplAstNode[], pipes: Map>>, - schemas: SchemaMetadata[], sourceMapping: TemplateSourceMapping, - file: ParseSourceFile): void { + schemas: SchemaMetadata[], sourceMapping: TemplateSourceMapping, file: ParseSourceFile, + parseErrors: ParseError[]|null): void { if (!this.host.shouldCheckComponent(ref.node)) { 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 overrideTemplate = this.host.getTemplateOverride(sfPath, ref.node); 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 // unsupported inlining operations. let missingInlines: ClassDeclaration[] = []; - const fileData = this.dataForFile(ref.node.getSourceFile()); - const shimData = this.pendingShimForComponent(ref.node); const boundTarget = binder.bind({template}); // 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, { template, boundTarget, + templateDiagnostics, }); const tcbRequiresInline = requiresInlineTypeCheckBlock(ref.node, pipes); @@ -435,6 +454,26 @@ export class TypeCheckContextImpl implements TypeCheckContext { 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); + }); + } } /** diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/test_utils.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/test_utils.ts index 862c8c2a0c..bc55caad92 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/test_utils.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/test_utils.ts @@ -407,7 +407,7 @@ export function setup(targets: TypeCheckingTarget[], overrides: { node: classRef.node.name, }; - ctx.addTemplate(classRef, binder, nodes, pipes, [], sourceMapping, templateFile); + ctx.addTemplate(classRef, binder, nodes, pipes, [], sourceMapping, templateFile, errors); } } }); diff --git a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts index b44016d070..cb2a939196 100644 --- a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts +++ b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts @@ -7577,10 +7577,13 @@ export const Foo = Foo__PRE_R3__; env.write('test.ts', ` import {Component} from '@angular/core'; @Component({ - template: '', + template: '', selector: 'test-cmp', }) - export class TestCmp {} + export class TestCmp { + x = null; + y = null; + } `); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); @@ -7591,19 +7594,41 @@ export const Foo = Foo__PRE_R3__; env.write('test.ts', ` import {Component} from '@angular/core'; @Component({ - template: '', selector: 'test-cmp', }) - export class TestCmp {} + export class TestCmp { + x = null; + } `); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); 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: \` {{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', () => { - it('should report helpful error message on nested i18n sections', () => { + it('reports a diagnostics on nested i18n sections', () => { env.write('test.ts', ` import {Component} from '@angular/core'; @Component({ @@ -7625,7 +7650,7 @@ export const Foo = Foo__PRE_R3__; .toEqual('
Content
'); }); - 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', ` import {Component} from '@angular/core'; @Component({ @@ -7647,7 +7672,7 @@ export const Foo = Foo__PRE_R3__; .toEqual('
Content
'); }); - it('report a diagnostic on nested i18n sections represented with s', () => { + it('reports a diagnostic on nested i18n sections represented with s', () => { env.write('test.ts', ` import {Component} from '@angular/core'; @Component({ diff --git a/packages/language-service/ivy/test/diagnostic_spec.ts b/packages/language-service/ivy/test/diagnostic_spec.ts index 0031673450..b837c18ce7 100644 --- a/packages/language-service/ivy/test/diagnostic_spec.ts +++ b/packages/language-service/ivy/test/diagnostic_spec.ts @@ -13,8 +13,6 @@ import * as ts from 'typescript'; import {createModuleWithDeclarations} from './test_utils'; describe('getSemanticDiagnostics', () => { - let env: LanguageServiceTestEnvironment; - beforeEach(() => { initMockFileSystem('Native'); }); @@ -31,7 +29,7 @@ describe('getSemanticDiagnostics', () => { export class AppComponent {} ` }; - env = createModuleWithDeclarations([appFile]); + const env = createModuleWithDeclarations([appFile]); const diags = env.ngLS.getSemanticDiagnostics(absoluteFrom('/app.ts')); expect(diags.length).toEqual(0); @@ -49,7 +47,7 @@ describe('getSemanticDiagnostics', () => { export class AppComponent {} ` }; - env = createModuleWithDeclarations([appFile]); + const env = createModuleWithDeclarations([appFile]); const diags = env.ngLS.getSemanticDiagnostics(absoluteFrom('/app.ts')); 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')); expect(diags).toEqual([]); }); @@ -97,7 +95,7 @@ describe('getSemanticDiagnostics', () => { }; 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')); expect(diags.length).toBe(1); const {category, file, start, length, messageText} = diags[0]; @@ -105,4 +103,75 @@ describe('getSemanticDiagnostics', () => { expect(file?.fileName).toBe('/app.html'); 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' + ]); + }); });