Skip to content

Commit abd1bc8

Browse files
atscottAndrewKushnir
authored andcommitted
fix(compiler): correct spans when parsing bindings with comments (#44785)
The previous fix for correcting spans with comments in 59eef29 had the unfortunate side effect of _breaking_ the spans with comments when there was leading whitespace. This happened because the previous fix was testing one without a comment, identifying that the offset shouldn't have anything added to it, and then removing that offset adjustment (`offsets[i] + (expressionText.length - sourceToLex.length)`). Upon further investigation, this offset adjustment _was actually necessary_ for when the input had comments, but this was only because the `stripComments` function used `trim` to remove whitespace for these cases. This is the real problem -- not only does it create a ton of confusion but also it means that the behavior of the lexer and resulting spans is different between inputs with comments and inputs without comments. After reviewing how the `inputLength` of `_ParseAST` was used, it appears that the correct behavior would be to _not_ trim the input. The `inputLength` is used to advance the current index beyond points which have been processed. This _should_ include any whitespace. Additionally, `inputLength` doesn't appear to be needed at all. When there was no comment in the input, it was always equal to the `input.length` anyways. When there _is_ a comment, it should include that comment anyways to advance the index beyond the comment. PR Close #44785
1 parent 7316e72 commit abd1bc8

File tree

3 files changed

+38
-18
lines changed

3 files changed

+38
-18
lines changed

packages/compiler-cli/src/ngtsc/indexer/test/template_spec.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,19 @@ runInEachFileSystem(() => {
6262
});
6363
});
6464

65+
it('should handle whitespace and comments in interpolations', () => {
66+
const template = '{{ foo // comment }}';
67+
const refs = getTemplateIdentifiers(bind(template));
68+
69+
const [ref] = Array.from(refs);
70+
expect(ref).toEqual({
71+
name: 'foo',
72+
kind: IdentifierKind.Property,
73+
span: new AbsoluteSourceSpan(5, 8),
74+
target: null,
75+
});
76+
});
77+
6578
it('should generate nothing in empty template', () => {
6679
const template = '';
6780
const refs = getTemplateIdentifiers(bind(template));

packages/compiler/src/expression_parser/parser.ts

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,7 @@ export class Parser {
4141
const sourceToLex = this._stripComments(input);
4242
const tokens = this._lexer.tokenize(sourceToLex);
4343
const ast =
44-
new _ParseAST(
45-
input, location, absoluteOffset, tokens, sourceToLex.length, true, this.errors, 0)
46-
.parseChain();
44+
new _ParseAST(input, location, absoluteOffset, tokens, true, this.errors, 0).parseChain();
4745
return new ASTWithSource(ast, input, location, absoluteOffset, this.errors);
4846
}
4947

@@ -90,8 +88,7 @@ export class Parser {
9088
this._checkNoInterpolation(input, location, interpolationConfig);
9189
const sourceToLex = this._stripComments(input);
9290
const tokens = this._lexer.tokenize(sourceToLex);
93-
return new _ParseAST(
94-
input, location, absoluteOffset, tokens, sourceToLex.length, false, this.errors, 0)
91+
return new _ParseAST(input, location, absoluteOffset, tokens, false, this.errors, 0)
9592
.parseChain();
9693
}
9794

@@ -138,8 +135,8 @@ export class Parser {
138135
absoluteValueOffset: number): TemplateBindingParseResult {
139136
const tokens = this._lexer.tokenize(templateValue);
140137
const parser = new _ParseAST(
141-
templateValue, templateUrl, absoluteValueOffset, tokens, templateValue.length,
142-
false /* parseAction */, this.errors, 0 /* relative offset */);
138+
templateValue, templateUrl, absoluteValueOffset, tokens, false /* parseAction */,
139+
this.errors, 0 /* relative offset */);
143140
return parser.parseTemplateBindings({
144141
source: templateKey,
145142
span: new AbsoluteSourceSpan(absoluteKeyOffset, absoluteKeyOffset + templateKey.length),
@@ -159,10 +156,9 @@ export class Parser {
159156
const expressionText = expressions[i].text;
160157
const sourceToLex = this._stripComments(expressionText);
161158
const tokens = this._lexer.tokenize(sourceToLex);
162-
const ast = new _ParseAST(
163-
input, location, absoluteOffset, tokens, sourceToLex.length, false,
164-
this.errors, offsets[i])
165-
.parseChain();
159+
const ast =
160+
new _ParseAST(input, location, absoluteOffset, tokens, false, this.errors, offsets[i])
161+
.parseChain();
166162
expressionNodes.push(ast);
167163
}
168164

@@ -180,7 +176,7 @@ export class Parser {
180176
const sourceToLex = this._stripComments(expression);
181177
const tokens = this._lexer.tokenize(sourceToLex);
182178
const ast = new _ParseAST(
183-
expression, location, absoluteOffset, tokens, sourceToLex.length,
179+
expression, location, absoluteOffset, tokens,
184180
/* parseAction */ false, this.errors, 0)
185181
.parseChain();
186182
const strings = ['', '']; // The prefix and suffix strings are both empty
@@ -275,7 +271,7 @@ export class Parser {
275271

276272
private _stripComments(input: string): string {
277273
const i = this._commentStart(input);
278-
return i != null ? input.substring(0, i).trim() : input;
274+
return i != null ? input.substring(0, i) : input;
279275
}
280276

281277
private _commentStart(input: string): number|null {
@@ -392,8 +388,8 @@ export class _ParseAST {
392388

393389
constructor(
394390
public input: string, public location: string, public absoluteOffset: number,
395-
public tokens: Token[], public inputLength: number, public parseAction: boolean,
396-
private errors: ParserError[], private offset: number) {}
391+
public tokens: Token[], public parseAction: boolean, private errors: ParserError[],
392+
private offset: number) {}
397393

398394
peek(offset: number): Token {
399395
const i = this.index + offset;
@@ -429,7 +425,7 @@ export class _ParseAST {
429425
// No tokens have been processed yet; return the next token's start or the length of the input
430426
// if there is no token.
431427
if (this.tokens.length === 0) {
432-
return this.inputLength + this.offset;
428+
return this.input.length + this.offset;
433429
}
434430
return this.next.index + this.offset;
435431
}
@@ -587,7 +583,7 @@ export class _ParseAST {
587583
if (exprs.length == 0) {
588584
// We have no expressions so create an empty expression that spans the entire input length
589585
const artificialStart = this.offset;
590-
const artificialEnd = this.offset + this.inputLength;
586+
const artificialEnd = this.offset + this.input.length;
591587
return new EmptyExpr(
592588
this.span(artificialStart, artificialEnd),
593589
this.sourceSpan(artificialStart, artificialEnd));
@@ -623,7 +619,7 @@ export class _ParseAST {
623619
//
624620
// Therefore, we push the end of the `ParseSpan` for this pipe all the way up to the
625621
// beginning of the next token, or until the end of input if the next token is EOF.
626-
fullSpanEnd = this.next.index !== -1 ? this.next.index : this.inputLength + this.offset;
622+
fullSpanEnd = this.next.index !== -1 ? this.next.index : this.input.length + this.offset;
627623

628624
// The `nameSpan` for an empty pipe name is zero-length at the end of any whitespace
629625
// beyond the pipe character.

packages/compiler/test/render3/r3_ast_absolute_span_spec.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,17 @@ describe('expression AST absolute source spans', () => {
1717
.toContain(['foo', new AbsoluteSourceSpan(2, 5)]);
1818
});
1919

20+
it('should handle whitespace in interpolation', () => {
21+
expect(humanizeExpressionSource(parse('{{ foo }}', {preserveWhitespaces: true}).nodes))
22+
.toContain(['foo', new AbsoluteSourceSpan(4, 7)]);
23+
});
24+
25+
it('should handle whitespace and comment in interpolation', () => {
26+
expect(humanizeExpressionSource(
27+
parse('{{ foo // comment }}', {preserveWhitespaces: true}).nodes))
28+
.toContain(['foo', new AbsoluteSourceSpan(4, 7)]);
29+
});
30+
2031
it('should handle comment in an action binding', () => {
2132
expect(humanizeExpressionSource(parse('<button (click)="foo = true // comment">Save</button>', {
2233
preserveWhitespaces: true

0 commit comments

Comments
 (0)