fix(compiler): don't report parse error for interpolation inside string in property binding (#40267)
Currently we check whether a property binding contains an interpolation using a regex so that we can throw an error. The problem is that the regex doesn't account for quotes which means that something like `[prop]="'{{ foo }}'"` will be considered an error, even though it's not actually an interpolation. These changes build on top of the logic from #39826 to account for interpolation characters inside quotes. Fixes #39601. PR Close #40267
This commit is contained in:
parent
6a9d7e5969
commit
6abc13330b
|
@ -8,7 +8,6 @@
|
||||||
|
|
||||||
import * as chars from '../chars';
|
import * as chars from '../chars';
|
||||||
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../ml_parser/interpolation_config';
|
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../ml_parser/interpolation_config';
|
||||||
import {escapeRegExp} from '../util';
|
|
||||||
|
|
||||||
import {AbsoluteSourceSpan, AST, AstVisitor, ASTWithSource, Binary, BindingPipe, Chain, Conditional, EmptyExpr, ExpressionBinding, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralMapKey, LiteralPrimitive, MethodCall, NonNullAssert, ParserError, ParseSpan, PrefixNot, PropertyRead, PropertyWrite, Quote, RecursiveAstVisitor, SafeMethodCall, SafePropertyRead, TemplateBinding, TemplateBindingIdentifier, ThisReceiver, Unary, VariableBinding} from './ast';
|
import {AbsoluteSourceSpan, AST, AstVisitor, ASTWithSource, Binary, BindingPipe, Chain, Conditional, EmptyExpr, ExpressionBinding, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralMapKey, LiteralPrimitive, MethodCall, NonNullAssert, ParserError, ParseSpan, PrefixNot, PropertyRead, PropertyWrite, Quote, RecursiveAstVisitor, SafeMethodCall, SafePropertyRead, TemplateBinding, TemplateBindingIdentifier, ThisReceiver, Unary, VariableBinding} from './ast';
|
||||||
import {EOF, isIdentifier, isQuote, Lexer, Token, TokenType} from './lexer';
|
import {EOF, isIdentifier, isQuote, Lexer, Token, TokenType} from './lexer';
|
||||||
|
@ -30,20 +29,6 @@ export class TemplateBindingParseResult {
|
||||||
public errors: ParserError[]) {}
|
public errors: ParserError[]) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultInterpolateRegExp = _createInterpolateRegExp(DEFAULT_INTERPOLATION_CONFIG);
|
|
||||||
function _getInterpolateRegExp(config: InterpolationConfig): RegExp {
|
|
||||||
if (config === DEFAULT_INTERPOLATION_CONFIG) {
|
|
||||||
return defaultInterpolateRegExp;
|
|
||||||
} else {
|
|
||||||
return _createInterpolateRegExp(config);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function _createInterpolateRegExp(config: InterpolationConfig): RegExp {
|
|
||||||
const pattern = escapeRegExp(config.start) + '([\\s\\S]*?)' + escapeRegExp(config.end);
|
|
||||||
return new RegExp(pattern, 'g');
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Parser {
|
export class Parser {
|
||||||
private errors: ParserError[] = [];
|
private errors: ParserError[] = [];
|
||||||
|
|
||||||
|
@ -247,7 +232,7 @@ export class Parser {
|
||||||
// parse from starting {{ to ending }} while ignoring content inside quotes.
|
// parse from starting {{ to ending }} while ignoring content inside quotes.
|
||||||
const fullStart = i;
|
const fullStart = i;
|
||||||
const exprStart = fullStart + interpStart.length;
|
const exprStart = fullStart + interpStart.length;
|
||||||
const exprEnd = this._getExpressiondEndIndex(input, interpEnd, exprStart);
|
const exprEnd = this._getInterpolationEndIndex(input, interpEnd, exprStart);
|
||||||
if (exprEnd === -1) {
|
if (exprEnd === -1) {
|
||||||
// Could not find the end of the interpolation; do not parse an expression.
|
// Could not find the end of the interpolation; do not parse an expression.
|
||||||
// Instead we should extend the content on the last raw string.
|
// Instead we should extend the content on the last raw string.
|
||||||
|
@ -315,59 +300,71 @@ export class Parser {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _checkNoInterpolation(
|
private _checkNoInterpolation(input: string, location: string, {start, end}: InterpolationConfig):
|
||||||
input: string, location: string, interpolationConfig: InterpolationConfig): void {
|
void {
|
||||||
const regexp = _getInterpolateRegExp(interpolationConfig);
|
let startIndex = -1;
|
||||||
const parts = input.split(regexp);
|
let endIndex = -1;
|
||||||
if (parts.length > 1) {
|
|
||||||
|
for (const charIndex of this._forEachUnquotedChar(input, 0)) {
|
||||||
|
if (startIndex === -1) {
|
||||||
|
if (input.startsWith(start)) {
|
||||||
|
startIndex = charIndex;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
endIndex = this._getInterpolationEndIndex(input, end, charIndex);
|
||||||
|
if (endIndex > -1) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startIndex > -1 && endIndex > -1) {
|
||||||
this._reportError(
|
this._reportError(
|
||||||
`Got interpolation (${interpolationConfig.start}${
|
`Got interpolation (${start}${end}) where expression was expected`, input,
|
||||||
interpolationConfig.end}) where expression was expected`,
|
`at column ${startIndex} in`, location);
|
||||||
input,
|
|
||||||
`at column ${this._findInterpolationErrorColumn(parts, 1, interpolationConfig)} in`,
|
|
||||||
location);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _findInterpolationErrorColumn(
|
|
||||||
parts: string[], partInErrIdx: number, interpolationConfig: InterpolationConfig): number {
|
|
||||||
let errLocation = '';
|
|
||||||
for (let j = 0; j < partInErrIdx; j++) {
|
|
||||||
errLocation += j % 2 === 0 ?
|
|
||||||
parts[j] :
|
|
||||||
`${interpolationConfig.start}${parts[j]}${interpolationConfig.end}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return errLocation.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds the index of the end of an interpolation expression
|
* Finds the index of the end of an interpolation expression
|
||||||
* while ignoring comments and quoted content.
|
* while ignoring comments and quoted content.
|
||||||
*/
|
*/
|
||||||
private _getExpressiondEndIndex(input: string, expressionEnd: string, start: number): number {
|
private _getInterpolationEndIndex(input: string, expressionEnd: string, start: number): number {
|
||||||
|
for (const charIndex of this._forEachUnquotedChar(input, start)) {
|
||||||
|
if (input.startsWith(expressionEnd, charIndex)) {
|
||||||
|
return charIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nothing else in the expression matters after we've
|
||||||
|
// hit a comment so look directly for the end token.
|
||||||
|
if (input.startsWith('//', charIndex)) {
|
||||||
|
return input.indexOf(expressionEnd, charIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generator used to iterate over the character indexes of a string that are outside of quotes.
|
||||||
|
* @param input String to loop through.
|
||||||
|
* @param start Index within the string at which to start.
|
||||||
|
*/
|
||||||
|
private * _forEachUnquotedChar(input: string, start: number) {
|
||||||
let currentQuote: string|null = null;
|
let currentQuote: string|null = null;
|
||||||
let escapeCount = 0;
|
let escapeCount = 0;
|
||||||
for (let i = start; i < input.length; i++) {
|
for (let i = start; i < input.length; i++) {
|
||||||
const char = input[i];
|
const char = input[i];
|
||||||
// Skip the characters inside quotes. Note that we only care about the
|
// Skip the characters inside quotes. Note that we only care about the outer-most
|
||||||
// outer-most quotes matching up and we need to account for escape characters.
|
// quotes matching up and we need to account for escape characters.
|
||||||
if (isQuote(input.charCodeAt(i)) && (currentQuote === null || currentQuote === char) &&
|
if (isQuote(input.charCodeAt(i)) && (currentQuote === null || currentQuote === char) &&
|
||||||
escapeCount % 2 === 0) {
|
escapeCount % 2 === 0) {
|
||||||
currentQuote = currentQuote === null ? char : null;
|
currentQuote = currentQuote === null ? char : null;
|
||||||
} else if (currentQuote === null) {
|
} else if (currentQuote === null) {
|
||||||
if (input.startsWith(expressionEnd, i)) {
|
yield i;
|
||||||
return i;
|
|
||||||
}
|
|
||||||
// Nothing else in the expression matters after we've
|
|
||||||
// hit a comment so look directly for the end token.
|
|
||||||
if (input.startsWith('//', i)) {
|
|
||||||
return input.indexOf(expressionEnd, i);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
escapeCount = char === '\\' ? escapeCount + 1 : 0;
|
escapeCount = char === '\\' ? escapeCount + 1 : 0;
|
||||||
}
|
}
|
||||||
return -1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -314,6 +314,13 @@ describe('parser', () => {
|
||||||
it('should report when encountering interpolation', () => {
|
it('should report when encountering interpolation', () => {
|
||||||
expectActionError('{{a()}}', 'Got interpolation ({{}}) where expression was expected');
|
expectActionError('{{a()}}', 'Got interpolation ({{}}) where expression was expected');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not report interpolation inside a string', () => {
|
||||||
|
expect(parseAction(`"{{a()}}"`).errors).toEqual([]);
|
||||||
|
expect(parseAction(`'{{a()}}'`).errors).toEqual([]);
|
||||||
|
expect(parseAction(`"{{a('\\"')}}"`).errors).toEqual([]);
|
||||||
|
expect(parseAction(`'{{a("\\'")}}'`).errors).toEqual([]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('parse spans', () => {
|
describe('parse spans', () => {
|
||||||
|
@ -486,6 +493,13 @@ describe('parser', () => {
|
||||||
expectBindingError('{{a.b}}', 'Got interpolation ({{}}) where expression was expected');
|
expectBindingError('{{a.b}}', 'Got interpolation ({{}}) where expression was expected');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not report interpolation inside a string', () => {
|
||||||
|
expect(parseBinding(`"{{exp}}"`).errors).toEqual([]);
|
||||||
|
expect(parseBinding(`'{{exp}}'`).errors).toEqual([]);
|
||||||
|
expect(parseBinding(`'{{\\"}}'`).errors).toEqual([]);
|
||||||
|
expect(parseBinding(`'{{\\'}}'`).errors).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
it('should parse conditional expression', () => {
|
it('should parse conditional expression', () => {
|
||||||
checkBinding('a < b ? a : b');
|
checkBinding('a < b ? a : b');
|
||||||
});
|
});
|
||||||
|
@ -952,6 +966,13 @@ describe('parser', () => {
|
||||||
'Got interpolation ({{}}) where expression was expected');
|
'Got interpolation ({{}}) where expression was expected');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not report interpolation inside a string', () => {
|
||||||
|
expect(parseSimpleBinding(`"{{exp}}"`).errors).toEqual([]);
|
||||||
|
expect(parseSimpleBinding(`'{{exp}}'`).errors).toEqual([]);
|
||||||
|
expect(parseSimpleBinding(`'{{\\"}}'`).errors).toEqual([]);
|
||||||
|
expect(parseSimpleBinding(`'{{\\'}}'`).errors).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
it('should report when encountering field write', () => {
|
it('should report when encountering field write', () => {
|
||||||
expectError(validate(parseSimpleBinding('a = b')), 'Bindings cannot contain assignments');
|
expectError(validate(parseSimpleBinding('a = b')), 'Bindings cannot contain assignments');
|
||||||
});
|
});
|
||||||
|
|
|
@ -623,4 +623,26 @@ describe('property bindings', () => {
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
}).not.toThrow();
|
}).not.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should allow quoted binding syntax inside property binding', () => {
|
||||||
|
@Component({template: `<span [id]="'{{ id }}'"></span>`})
|
||||||
|
class Comp {
|
||||||
|
}
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({declarations: [Comp]});
|
||||||
|
const fixture = TestBed.createComponent(Comp);
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(fixture.nativeElement.querySelector('span').id).toBe('{{ id }}');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow quoted binding syntax with escaped quotes inside property binding', () => {
|
||||||
|
@Component({template: `<span [id]="'{{ \\' }}'"></span>`})
|
||||||
|
class Comp {
|
||||||
|
}
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({declarations: [Comp]});
|
||||||
|
const fixture = TestBed.createComponent(Comp);
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(fixture.nativeElement.querySelector('span').id).toBe('{{ \' }}');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue