8000 fix(compiler): produce more accurate errors for interpolations by crisbeto · Pull Request #62258 · angular/angular · GitHub
[go: up one dir, main page]

Skip to content

fix(compiler): produce more accurate errors for interpolations #62258

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
fix(compiler): produce more accurate errors for interpolations
Currently when there's a parser error in interpolated text, the compiler reports an error on the entire text node. This can be really noisy in long strings.

These changes switch to reporting the errors on the specific expressions that caused them.
  • Loading branch information
crisbeto committed Jun 25, 2025
commit 497e71e597865caaf3ae0a23660d9d8925d4e73c
2 changes: 1 addition & 1 deletion packages/compiler-cli/test/ngtsc/ngtsc_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10022,7 +10022,7 @@ runInEachFileSystem((os: string) => {
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}}]',
'Parser Error: Bindings cannot contain assignments at column 5 in [x = 2]',
);
});
});
Expand Down
8 changes: 6 additions & 2 deletions packages/compiler/src/expression_parser/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,12 +268,16 @@ export class Parser {
const expressionNodes: AST[] = [];

for (let i = 0; i < expressions.length; ++i) {
// If we have a token for the specific expression, it's preferrable to use it because it
// allows us to produce more accurate error messages. The expressions are always at the odd
// indexes inside the tokens.
const expressionSpan = interpolatedTokens?.[i * 2 + 1]?.sourceSpan;
const expressionText = expressions[i].text;
const sourceToLex = this._stripComments(expressionText);
const tokens = this._lexer.tokenize(sourceToLex);
const ast = new _ParseAST(
input,
parseSourceSpan,
expressionSpan ? expressionText : input,
expressionSpan || parseSourceSpan,
absoluteOffset,
tokens,
ParseFlags.None,
Expand Down
35 changes: 35 additions & 0 deletions packages/compiler/test/render3/r3_template_transform_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -784,6 +784,41 @@ describe('R3 template transform', () => {
expect(errors[1].msg).toContain('Invalid character [#]');
expect(errors[2].msg).toContain(`Unexpected token ')'`);
});

it('should report parsing errors on the specific interpolated expressions', () => {
const errors = parse(
`
bunch of text bunch of text bunch of text bunch of text bunch of text bunch of text
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For reference, the current code will report 3 errors, each of which covers the entire string including the plain text. With the changes the errors are only reported on interpolations.

bunch of text bunch of text bunch of text bunch of text

{{foo[0}} bunch of text bunch of text bunch of text bunch of text {{.bar}}

bunch of text
bunch of text
bunch of text
bunch of text
bunch of text {{one + #two + baz}}
`,
{
ignoreError: true,
},
).errors;

expect(errors.map((e) => e.span.toString())).toEqual([
'{{foo[0}}',
'{{.bar}}',
'{{one + #two + baz}}',
]);

expect(errors.map((e) => e.msg)).toEqual([
jasmine.stringContaining('Missing expected ] at the end of the expression [foo[0]'),
jasmine.stringContaining('Unexpected token . at column 1 in [.bar]'),
jasmine.stringContaining(
'Private identifiers are not supported. Unexpected private identifier: ' +
'#two at column 7 in [one + #two + baz]',
),
]);
});
});

describe('Ignored elements', () => {
Expand Down
12 changes: 6 additions & 6 deletions packages/language-service/test/diagnostic_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ describe('getSemanticDiagnostics', () => {
expect(category).toBe(ts.DiagnosticCategory.Error);
expect(file?.fileName).toBe('/test/app.html');
expect(messageText).toContain(
`Parser Error: Bindings cannot contain assignments at column 8 in [{{nope = true}}]`,
`Parser Error: Bindings cannot contain assignments at column 8 in [nope = true]`,
);
});

Expand Down Expand Up @@ -231,13 +231,13 @@ describe('getSemanticDiagnostics', () => {
'app.ts': `
import {Component, NgModule} from '@angular/core';

@Component({
@Component({
templateUrl: './app1.html',
standalone: false,
})
export class AppComponent1 { nope = false; }

@Component({
@Component({
templateUrl: './app2.html',
standalone: false,
})
Expand All @@ -262,13 +262,13 @@ describe('getSemanticDiagnostics', () => {
const diags1 = project.getDiagnosticsForFile('app1.html');
expect(diags1.length).toBe(1);
expect(diags1[0].messageText).toBe(
'Parser Error: Bindings cannot contain assignments at column 8 in [{{nope = false}}] in /test/app1.html@0:0',
'Parser Error: Bindings cannot contain assignments at column 8 in [nope = false] in /test/app1.html@0:0',
);

const diags2 = project.getDiagnosticsForFile('app2.html');
expect(diags2.length).toBe(1);
expect(diags2[0].messageText).toBe(
'Parser Error: Bindings cannot contain assignments at column 8 in [{{nope = true}}] in /test/app2.html@0:0',
'Parser Error: Bindings cannot contain assignments at column 8 in [nope = true] in /test/app2.html@0:0',
);
});

Expand Down Expand Up @@ -386,7 +386,7 @@ describe('getSemanticDiagnostics', () => {
const files = {
'app.ts': `
import {Component} from '@angular/core';
@Component({
@Component({
template: '',
standalone: false,
})
Expand Down
Loading
0