Skip to content

Commit 726ec88

Browse files
JoostKmhevery
authored andcommitted
fix(compiler): exclude trailing whitespace from element source spans (#40513)
If the template parse option `leadingTriviaChars` is configured to consider whitespace as trivia, any trailing whitespace of an element would be considered as leading trivia of the subsequent element, such that its `start` span would start _after_ the whitespace. This means that the start span cannot be used to mark the end of the current element, as its trailing whitespace would then be included in its span. Instead, the full start of the subsequent element should be used. To harden the tests that for the Ivy parser, the test utility `parseR3` has been adjusted to use the same configuration for `leadingTriviaChars` as would be the case in its production counterpart `parseTemplate`. This uncovered another bug in offset handling of the interpolation parser, where the absolute offset was computed from the start source span (which excludes leading trivia) whereas the interpolation expression would include the leading trivia. As such, the absolute offset now also uses the full start span. Fixes #39148 PR Close #40513
1 parent abdf25c commit 726ec88

File tree

12 files changed

+62
-17
lines changed

12 files changed

+62
-17
lines changed

packages/compiler-cli/src/ngtsc/annotations/src/component.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -879,6 +879,9 @@ export class ComponentDecoratorHandler implements
879879
//
880880
// In order to guarantee the correctness of diagnostics, templates are parsed a second time
881881
// with the above options set to preserve source mappings.
882+
//
883+
// Note: template parse options should be aligned with `template_target_spec.ts` and
884+
// `TemplateTypeCheckerImpl.overrideComponentTemplate`.
882885

883886
const {nodes: diagNodes} = parseTemplate(templateStr, template.sourceMapUrl, {
884887
preserveWhitespaces: true,

packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,11 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
154154
overrideComponentTemplate(component: ts.ClassDeclaration, template: string):
155155
{nodes: TmplAstNode[], errors: ParseError[]|null} {
156156
const {nodes, errors} = parseTemplate(template, 'override.html', {
157+
// Set `leadingTriviaChars` and `preserveWhitespaces` such that whitespace is not stripped
158+
// and fully accounted for in source spans. Without these flags the source spans can be
159+
// inaccurate.
160+
// Note: template parse options should be aligned with `template_target_spec.ts` and the
161+
// `diagNodes` in `ComponentDecoratorHandler._parseTemplate`.
157162
preserveWhitespaces: true,
158163
leadingTriviaChars: [],
159164
});

packages/compiler-cli/test/compliance/test_cases/source_mapping/external_templates/escaped_chars.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
i0.ɵɵelementStart(0, "div") // SOURCE: "/escaped_chars.html" <div>\r\n
1+
i0.ɵɵelementStart(0, "div") // SOURCE: "/escaped_chars.html" <div>
22
33
// NOTE: the `\\r\\n` at the end of the next line will be unescaped to `\r\n`. If it was just `\r\n` it would get unescaped to the actual characters.
44
i0.ɵɵtext(1, " Some Message Encoded character: \uD83D\uDE80\\n") // SOURCE: "/escaped_chars.html" Some Message\r\n Encoded character: 🚀\\r\\n

packages/compiler-cli/test/compliance/test_cases/source_mapping/inline_templates/i18n_message_element_whitespace.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99
}:CLOSE_PARAGRAPH: post-p\n` // SOURCE: "/i18n_message_element_whitespace.ts" post-p\\n
1010
1111

12-
i0.ɵɵelementStart(0, "div") // SOURCE: "/i18n_message_element_whitespace.ts" <div i18n>\\n
12+
i0.ɵɵelementStart(0, "div") // SOURCE: "/i18n_message_element_whitespace.ts" <div i18n>
1313
14-
i0.ɵɵi18nStart(1, 0) // SOURCE: "/i18n_message_element_whitespace.ts" <div i18n>\\n
14+
i0.ɵɵi18nStart(1, 0) // SOURCE: "/i18n_message_element_whitespace.ts" <div i18n>
1515
1616
i0.ɵɵelement(2, "p") // SOURCE: "/i18n_message_element_whitespace.ts" <p>\\n
1717

packages/compiler-cli/test/ngtsc/template_mapping_spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -432,7 +432,7 @@ runInEachFileSystem((os) => {
432432
const mappings = compileAndMap(
433433
`<div i18n title=" pre-title {{name}} post-title" i18n-title> pre-body {{greeting}} post-body</div>`);
434434
expectMapping(mappings, {
435-
source: '<div i18n title=" pre-title {{name}} post-title" i18n-title> ',
435+
source: '<div i18n title=" pre-title {{name}} post-title" i18n-title>',
436436
generated: 'i0.ɵɵelementStart(0, "div", 0)',
437437
sourceUrl: '../test.ts',
438438
});
@@ -491,12 +491,12 @@ runInEachFileSystem((os) => {
491491
// ivy instructions
492492
expectMapping(mappings, {
493493
sourceUrl: '../test.ts',
494-
source: '<div i18n>\n ',
494+
source: '<div i18n>',
495495
generated: 'i0.ɵɵelementStart(0, "div")',
496496
});
497497
expectMapping(mappings, {
498498
sourceUrl: '../test.ts',
499-
source: '<div i18n>\n ',
499+
source: '<div i18n>',
500500
generated: 'i0.ɵɵi18nStart(1, 0)',
501501
});
502502
expectMapping(mappings, {

packages/compiler/src/ml_parser/parser.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ class _TreeBuilder {
259259
this._advance();
260260
selfClosing = false;
261261
}
262-
const end = this._peek.sourceSpan.start;
262+
const end = this._peek.sourceSpan.fullStart;
263263
const span = new ParseSourceSpan(
264264
startTagToken.sourceSpan.start, end, startTagToken.sourceSpan.fullStart);
265265
// Create a separate `startSpan` because `span` will be modified when there is an `end` span.

packages/compiler/src/template_parser/binding_parser.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -120,16 +120,17 @@ export class BindingParser {
120120

121121
parseInterpolation(value: string, sourceSpan: ParseSourceSpan): ASTWithSource {
122122
const sourceInfo = sourceSpan.start.toString();
123+
const absoluteOffset = sourceSpan.fullStart.offset;
123124

124125
try {
125126
const ast = this._exprParser.parseInterpolation(
126-
value, sourceInfo, sourceSpan.start.offset, this._interpolationConfig)!;
127+
value, sourceInfo, absoluteOffset, this._interpolationConfig)!;
127128
if (ast) this._reportExpressionParserErrors(ast.errors, sourceSpan);
128129
this._checkPipes(ast, sourceSpan);
129130
return ast;
130131
} catch (e) {
131132
this._reportError(`${e}`, sourceSpan);
132-
return this._exprParser.wrapLiteralPrimitive('ERROR', sourceInfo, sourceSpan.start.offset);
133+
return this._exprParser.wrapLiteralPrimitive('ERROR', sourceInfo, absoluteOffset);
133134
}
134135
}
135136

@@ -140,16 +141,17 @@ export class BindingParser {
140141
*/
141142
parseInterpolationExpression(expression: string, sourceSpan: ParseSourceSpan): ASTWithSource {
142143
const sourceInfo = sourceSpan.start.toString();
144+
const absoluteOffset = sourceSpan.start.offset;
143145

144146
try {
145-
const ast = this._exprParser.parseInterpolationExpression(
146-
expression, sourceInfo, sourceSpan.start.offset);
147+
const ast =
148+
this._exprParser.parseInterpolationExpression(expression, sourceInfo, absoluteOffset);
147149
if (ast) this._reportExpressionParserErrors(ast.errors, sourceSpan);
148150
this._checkPipes(ast, sourceSpan);
149151
return ast;
150152
} catch (e) {
151153
this._reportError(`${e}`, sourceSpan);
152-
return this._exprParser.wrapLiteralPrimitive('ERROR', sourceInfo, sourceSpan.start.offset);
154+
return this._exprParser.wrapLiteralPrimitive('ERROR', sourceInfo, absoluteOffset);
153155
}
154156
}
155157

packages/compiler/test/ml_parser/html_parser_spec.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -709,6 +709,23 @@ import {humanizeDom, humanizeDomSourceSpans, humanizeLineColumn, humanizeNodes}
709709
]);
710710
});
711711

712+
it('should set the end source span excluding trailing whitespace whitespace', () => {
713+
expect(humanizeDomSourceSpans(
714+
parser.parse('<input type="text" />\n\n\n <span>\n</span>', 'TestComp', {
715+
leadingTriviaChars: [' ', '\n', '\r', '\t'],
716+
})))
717+
.toEqual([
718+
[
719+
html.Element, 'input', 0, '<input type="text" />', '<input type="text" />',
720+
'<input type="text" />'
721+
],
722+
[html.Attribute, 'type', 'text', 'type="text"'],
723+
[html.Text, '\n\n\n ', 0, ''],
724+
[html.Element, 'span', 0, '<span>\n</span>', '<span>', '</span>'],
725+
[html.Text, '\n', 1, ''],
726+
]);
727+
});
728+
712729
it('should not set the end source span for elements that are implicitly closed', () => {
713730
expect(humanizeDomSourceSpans(parser.parse('<div><p></div>', 'TestComp'))).toEqual([
714731
[html.Element, 'div', 0, '<div><p></div>', '<div>', '</div>'],

packages/compiler/test/render3/r3_ast_spans_spec.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,13 @@ describe('R3 AST source spans', () => {
148148
['TextAttribute', 'a', 'a', '<empty>'],
149149
]);
150150
});
151+
152+
it('is correct for self-closing elements with trailing whitespace', () => {
153+
expectFromHtml('<input />\n <span>\n</span>').toEqual([
154+
['Element', '<input />', '<input />', '<input />'],
155+
['Element', '<span>\n</span>', '<span>', '</span>'],
156+
]);
157+
});
151158
});
152159

153160
describe('bound text nodes', () => {

packages/compiler/test/render3/view/i18n_spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -523,7 +523,7 @@ describe('serializeI18nMessageForLocalize', () => {
523523
expect(humanizeSourceSpan(messageParts[3].sourceSpan)).toEqual('"" (29-29)');
524524

525525
expect(placeHolders[0].text).toEqual('START_BOLD_TEXT');
526-
expect(humanizeSourceSpan(placeHolders[0].sourceSpan)).toEqual('"<b> " (10-16)');
526+
expect(humanizeSourceSpan(placeHolders[0].sourceSpan)).toEqual('"<b>" (10-13)');
527527
expect(placeHolders[1].text).toEqual('INTERPOLATION');
528528
expect(humanizeSourceSpan(placeHolders[1].sourceSpan)).toEqual('"{{value}}" (16-25)');
529529
expect(placeHolders[2].text).toEqual('CLOSE_BOLD_TEXT');

0 commit comments

Comments
 (0)