Skip to content

Commit

Permalink
refactor(compiler): iteratively parse interpolations (#38977)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
ayazhafiz authored and josephperrott committed Oct 2, 2020
1 parent f50313f commit 6791cd7
Show file tree
Hide file tree
Showing 2 changed files with 70 additions and 28 deletions.
91 changes: 63 additions & 28 deletions packages/compiler/src/expression_parser/parser.ts
Expand Up @@ -185,47 +185,82 @@ export class Parser {
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(
input: string, location: string,
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): SplitInterpolation
|null {
const regexp = _getInterpolateRegExp(interpolationConfig);
const parts = input.split(regexp);
if (parts.length <= 1) {
return null;
}
const strings: string[] = [];
const expressions: string[] = [];
const offsets: number[] = [];
const stringSpans: {start: number, end: number}[] = [];
const expressionSpans: {start: number, end: number}[] = [];
let offset = 0;
for (let i = 0; i < parts.length; i++) {
const part: string = parts[i];
if (i % 2 === 0) {
// fixed string
let i = 0;
let atInterpolation = false;
let extendLastString = false;
let {start: interpStart, end: interpEnd} = interpolationConfig;
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);
const start = offset;
offset += part.length;
stringSpans.push({start, end: offset});
} 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});
stringSpans.push({start, end: i});

atInterpolation = true;
} else {
// parse from starting {{ to ending }}
const fullStart = i;
const exprStart = fullStart + interpStart.length;
const exprEnd = input.indexOf(interpEnd, exprStart);
if (exprEnd === -1) {
// Could not find the end of the interpolation; do not parse an expression.
// 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;
}
}
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 {
this._reportError(
'Blank expressions are not allowed in interpolated strings', input,
`at column ${this._findInterpolationErrorColumn(parts, i, interpolationConfig)} in`,
location);
expressions.push('$implicit');
offsets.push(offset);
expressionSpans.push({start: offset, end: offset});
strings.push(input.substring(i));
stringSpans.push({start: i, end: input.length});
}
}
return new SplitInterpolation(strings, stringSpans, expressions, expressionSpans, offsets);
return expressions.length === 0 ?
null :
new SplitInterpolation(strings, stringSpans, expressions, expressionSpans, offsets);
}

wrapLiteralPrimitive(input: string|null, location: any, absoluteOffset: number): ASTWithSource {
Expand Down
7 changes: 7 additions & 0 deletions packages/compiler/test/expression_parser/parser_spec.ts
Expand Up @@ -728,6 +728,13 @@ describe('parser', () => {
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', () => {
const ast = parseInterpolation('{{a}}')!.ast as Interpolation;
expect(ast.strings).toEqual(['', '']);
Expand Down

0 comments on commit 6791cd7

Please sign in to comment.