refactor(compiler): iteratively parse interpolations (#38977)

This patch refactors the interpolation parser to do so iteratively
rather than using a regex. Doing so prepares us for supporting granular
recovery on poorly-formed interpolations, for example when an
interpolation does not terminate (`{{ 1 + 2`) or is not terminated
properly (`{{ 1 + 2 {{ 2 + 3 }}`).

Part of #38596

PR Close #38977
This commit is contained in:
ayazhafiz 2020-09-24 11:22:12 -05:00 committed by Joey Perrott
parent 5dbf357224
commit 89c5255b8c
2 changed files with 70 additions and 28 deletions

View File

@ -185,47 +185,82 @@ export class Parser {
location, absoluteOffset, this.errors); location, absoluteOffset, this.errors);
} }
/**
* Splits a string of text into "raw" text segments and expressions present in interpolations in
* the string.
* Returns `null` if there are no interpolations, otherwise a
* `SplitInterpolation` with splits that look like
* <raw text> <expression> <raw text> ... <raw text> <expression> <raw text>
*/
splitInterpolation( splitInterpolation(
input: string, location: string, input: string, location: string,
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): SplitInterpolation interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): SplitInterpolation
|null { |null {
const regexp = _getInterpolateRegExp(interpolationConfig);
const parts = input.split(regexp);
if (parts.length <= 1) {
return null;
}
const strings: string[] = []; const strings: string[] = [];
const expressions: string[] = []; const expressions: string[] = [];
const offsets: number[] = []; const offsets: number[] = [];
const stringSpans: {start: number, end: number}[] = []; const stringSpans: {start: number, end: number}[] = [];
const expressionSpans: {start: number, end: number}[] = []; const expressionSpans: {start: number, end: number}[] = [];
let offset = 0; let i = 0;
for (let i = 0; i < parts.length; i++) { let atInterpolation = false;
const part: string = parts[i]; let extendLastString = false;
if (i % 2 === 0) { let {start: interpStart, end: interpEnd} = interpolationConfig;
// fixed string while (i < input.length) {
if (!atInterpolation) {
// parse until starting {{
const start = i;
i = input.indexOf(interpStart, i);
if (i === -1) {
i = input.length;
}
const part = input.substring(start, i);
strings.push(part); strings.push(part);
const start = offset; stringSpans.push({start, end: i});
offset += part.length;
stringSpans.push({start, end: offset}); atInterpolation = true;
} else if (part.trim().length > 0) {
const start = offset;
offset += interpolationConfig.start.length;
expressions.push(part);
offsets.push(offset);
offset += part.length + interpolationConfig.end.length;
expressionSpans.push({start, end: offset});
} else { } else {
this._reportError( // parse from starting {{ to ending }}
'Blank expressions are not allowed in interpolated strings', input, const fullStart = i;
`at column ${this._findInterpolationErrorColumn(parts, i, interpolationConfig)} in`, const exprStart = fullStart + interpStart.length;
location); const exprEnd = input.indexOf(interpEnd, exprStart);
expressions.push('$implicit'); if (exprEnd === -1) {
offsets.push(offset); // Could not find the end of the interpolation; do not parse an expression.
expressionSpans.push({start: offset, end: offset}); // Instead we should extend the content on the last raw string.
atInterpolation = false;
extendLastString = true;
break;
}
const fullEnd = exprEnd + interpEnd.length;
const part = input.substring(exprStart, exprEnd);
if (part.trim().length > 0) {
expressions.push(part);
} else {
this._reportError(
'Blank expressions are not allowed in interpolated strings', input,
`at column ${i} in`, location);
expressions.push('$implicit');
}
offsets.push(exprStart);
expressionSpans.push({start: fullStart, end: fullEnd});
i = fullEnd;
atInterpolation = false;
} }
} }
return new SplitInterpolation(strings, stringSpans, expressions, expressionSpans, offsets); if (!atInterpolation) {
// If we are now at a text section, add the remaining content as a raw string.
if (extendLastString) {
strings[strings.length - 1] += input.substring(i);
stringSpans[stringSpans.length - 1].end = input.length;
} else {
strings.push(input.substring(i));
stringSpans.push({start: i, end: input.length});
}
}
return expressions.length === 0 ?
null :
new SplitInterpolation(strings, stringSpans, expressions, expressionSpans, offsets);
} }
wrapLiteralPrimitive(input: string|null, location: any, absoluteOffset: number): ASTWithSource { wrapLiteralPrimitive(input: string|null, location: any, absoluteOffset: number): ASTWithSource {

View File

@ -728,6 +728,13 @@ describe('parser', () => {
expect(parseInterpolation('nothing')).toBe(null); expect(parseInterpolation('nothing')).toBe(null);
}); });
it('should not parse malformed interpolations as strings', () => {
const ast = parseInterpolation('{{a}} {{example}<!--->}')!.ast as Interpolation;
expect(ast.strings).toEqual(['', ' {{example}<!--->}']);
expect(ast.expressions.length).toEqual(1);
expect(ast.expressions[0].name).toEqual('a');
});
it('should parse no prefix/suffix interpolation', () => { it('should parse no prefix/suffix interpolation', () => {
const ast = parseInterpolation('{{a}}')!.ast as Interpolation; const ast = parseInterpolation('{{a}}')!.ast as Interpolation;
expect(ast.strings).toEqual(['', '']); expect(ast.strings).toEqual(['', '']);