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
*/
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<ComponentAnalysisData>):
@ -616,6 +593,9 @@ export class ComponentDecoratorHandler implements
compileFull(
node: ClassDeclaration, analysis: Readonly<ComponentAnalysisData>,
resolution: Readonly<ComponentResolutionData>, 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<ComponentAnalysisData>,
resolution: Readonly<ComponentResolutionData>): 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);

View File

@ -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 {

View File

@ -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<ClassDeclaration<ts.ClassDeclaration>>,
binder: R3TargetBinder<TypeCheckableDirectiveMeta>, template: TmplAstNode[],
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 {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<TemplateId, TmplAstNode[]>|null;
templateOverrides: Map<TemplateId, TemplateOverride>|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;

View File

@ -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<TemplateId, TemplateData>;
}
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<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
* 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<ts.ClassDeclaration>();
/**
* 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<ClassDeclaration<ts.ClassDeclaration>>,
binder: R3TargetBinder<TypeCheckableDirectiveMeta>, template: TmplAstNode[],
pipes: Map<string, Reference<ClassDeclaration<ts.ClassDeclaration>>>,
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);
});
}
}
/**

View File

@ -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);
}
}
});

View File

@ -7577,10 +7577,13 @@ export const Foo = Foo__PRE_R3__;
env.write('test.ts', `
import {Component} from '@angular/core';
@Component({
template: '<cmp [input]="x ? y">',
template: '<input [value]="x ? y"/>',
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: '<cmp [input]="x>',
template: '<input [value]="x/>',
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: \`<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', () => {
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('<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', `
import {Component} from '@angular/core';
@Component({
@ -7647,7 +7672,7 @@ export const Foo = Foo__PRE_R3__;
.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', `
import {Component} from '@angular/core';
@Component({

View File

@ -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'
]);
});
});