feat(ivy): record absolute position of template expressions (#31391)
Currently, template expressions and statements have their location recorded relative to the HTML element they are in, with no handle to absolute location in a source file except for a line/column location. However, the line/column location is also not entirely accurate, as it points an entire semantic expression, and not necessarily the start of an expression recorded by the expression parser. To support record of the source code expressions originate from, add a new `sourceSpan` field to `ASTWithSource` that records the absolute byte offset of an expression within a source code. Implement part 2 of [refactoring template parsing for stability](https://hackmd.io/@X3ECPVy-RCuVfba-pnvIpw/BkDUxaW84/%2FMA1oxh6jRXqSmZBcLfYdyw?type=book). PR Close #31391
This commit is contained in:
@ -56,7 +56,8 @@ export class BindingParser {
|
||||
Object.keys(dirMeta.hostProperties).forEach(propName => {
|
||||
const expression = dirMeta.hostProperties[propName];
|
||||
if (typeof expression === 'string') {
|
||||
this.parsePropertyBinding(propName, expression, true, sourceSpan, [], boundProps);
|
||||
this.parsePropertyBinding(
|
||||
propName, expression, true, sourceSpan, sourceSpan.start.offset, [], boundProps);
|
||||
} else {
|
||||
this._reportError(
|
||||
`Value of the host property binding "${propName}" needs to be a string representing an expression but got "${expression}" (${typeof expression})`,
|
||||
@ -100,20 +101,20 @@ export class BindingParser {
|
||||
const sourceInfo = sourceSpan.start.toString();
|
||||
|
||||
try {
|
||||
const ast =
|
||||
this._exprParser.parseInterpolation(value, sourceInfo, this._interpolationConfig) !;
|
||||
const ast = this._exprParser.parseInterpolation(
|
||||
value, sourceInfo, sourceSpan.start.offset, this._interpolationConfig) !;
|
||||
if (ast) this._reportExpressionParserErrors(ast.errors, sourceSpan);
|
||||
this._checkPipes(ast, sourceSpan);
|
||||
return ast;
|
||||
} catch (e) {
|
||||
this._reportError(`${e}`, sourceSpan);
|
||||
return this._exprParser.wrapLiteralPrimitive('ERROR', sourceInfo);
|
||||
return this._exprParser.wrapLiteralPrimitive('ERROR', sourceInfo, sourceSpan.start.offset);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse an inline template binding. ie `<tag *tplKey="<tplValue>">`
|
||||
parseInlineTemplateBinding(
|
||||
tplKey: string, tplValue: string, sourceSpan: ParseSourceSpan,
|
||||
tplKey: string, tplValue: string, sourceSpan: ParseSourceSpan, absoluteOffset: number,
|
||||
targetMatchableAttrs: string[][], targetProps: ParsedProperty[],
|
||||
targetVars: ParsedVariable[]) {
|
||||
const bindings = this._parseTemplateBindings(tplKey, tplValue, sourceSpan);
|
||||
@ -127,7 +128,8 @@ export class BindingParser {
|
||||
binding.key, binding.expression, sourceSpan, targetMatchableAttrs, targetProps);
|
||||
} else {
|
||||
targetMatchableAttrs.push([binding.key, '']);
|
||||
this.parseLiteralAttr(binding.key, null, sourceSpan, targetMatchableAttrs, targetProps);
|
||||
this.parseLiteralAttr(
|
||||
binding.key, null, sourceSpan, absoluteOffset, targetMatchableAttrs, targetProps);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -137,7 +139,8 @@ export class BindingParser {
|
||||
const sourceInfo = sourceSpan.start.toString();
|
||||
|
||||
try {
|
||||
const bindingsResult = this._exprParser.parseTemplateBindings(tplKey, tplValue, sourceInfo);
|
||||
const bindingsResult = this._exprParser.parseTemplateBindings(
|
||||
tplKey, tplValue, sourceInfo, sourceSpan.start.offset);
|
||||
this._reportExpressionParserErrors(bindingsResult.errors, sourceSpan);
|
||||
bindingsResult.templateBindings.forEach((binding) => {
|
||||
if (binding.expression) {
|
||||
@ -154,7 +157,7 @@ export class BindingParser {
|
||||
}
|
||||
|
||||
parseLiteralAttr(
|
||||
name: string, value: string|null, sourceSpan: ParseSourceSpan,
|
||||
name: string, value: string|null, sourceSpan: ParseSourceSpan, absoluteOffset: number,
|
||||
targetMatchableAttrs: string[][], targetProps: ParsedProperty[]) {
|
||||
if (isAnimationLabel(name)) {
|
||||
name = name.substring(1);
|
||||
@ -164,17 +167,18 @@ export class BindingParser {
|
||||
` Use property bindings (e.g. [@prop]="exp") or use an attribute without a value (e.g. @prop) instead.`,
|
||||
sourceSpan, ParseErrorLevel.ERROR);
|
||||
}
|
||||
this._parseAnimation(name, value, sourceSpan, targetMatchableAttrs, targetProps);
|
||||
this._parseAnimation(
|
||||
name, value, sourceSpan, absoluteOffset, targetMatchableAttrs, targetProps);
|
||||
} else {
|
||||
targetProps.push(new ParsedProperty(
|
||||
name, this._exprParser.wrapLiteralPrimitive(value, ''), ParsedPropertyType.LITERAL_ATTR,
|
||||
sourceSpan));
|
||||
name, this._exprParser.wrapLiteralPrimitive(value, '', absoluteOffset),
|
||||
ParsedPropertyType.LITERAL_ATTR, sourceSpan));
|
||||
}
|
||||
}
|
||||
|
||||
parsePropertyBinding(
|
||||
name: string, expression: string, isHost: boolean, sourceSpan: ParseSourceSpan,
|
||||
targetMatchableAttrs: string[][], targetProps: ParsedProperty[]) {
|
||||
absoluteOffset: number, targetMatchableAttrs: string[][], targetProps: ParsedProperty[]) {
|
||||
let isAnimationProp = false;
|
||||
if (name.startsWith(ANIMATE_PROP_PREFIX)) {
|
||||
isAnimationProp = true;
|
||||
@ -185,10 +189,11 @@ export class BindingParser {
|
||||
}
|
||||
|
||||
if (isAnimationProp) {
|
||||
this._parseAnimation(name, expression, sourceSpan, targetMatchableAttrs, targetProps);
|
||||
this._parseAnimation(
|
||||
name, expression, sourceSpan, absoluteOffset, targetMatchableAttrs, targetProps);
|
||||
} else {
|
||||
this._parsePropertyAst(
|
||||
name, this._parseBinding(expression, isHost, sourceSpan), sourceSpan,
|
||||
name, this._parseBinding(expression, isHost, sourceSpan, absoluteOffset), sourceSpan,
|
||||
targetMatchableAttrs, targetProps);
|
||||
}
|
||||
}
|
||||
@ -212,30 +217,33 @@ export class BindingParser {
|
||||
}
|
||||
|
||||
private _parseAnimation(
|
||||
name: string, expression: string|null, sourceSpan: ParseSourceSpan,
|
||||
name: string, expression: string|null, sourceSpan: ParseSourceSpan, absoluteOffset: number,
|
||||
targetMatchableAttrs: string[][], targetProps: ParsedProperty[]) {
|
||||
// This will occur when a @trigger is not paired with an expression.
|
||||
// For animations it is valid to not have an expression since */void
|
||||
// states will be applied by angular when the element is attached/detached
|
||||
const ast = this._parseBinding(expression || 'undefined', false, sourceSpan);
|
||||
const ast = this._parseBinding(expression || 'undefined', false, sourceSpan, absoluteOffset);
|
||||
targetMatchableAttrs.push([name, ast.source !]);
|
||||
targetProps.push(new ParsedProperty(name, ast, ParsedPropertyType.ANIMATION, sourceSpan));
|
||||
}
|
||||
|
||||
private _parseBinding(value: string, isHostBinding: boolean, sourceSpan: ParseSourceSpan):
|
||||
ASTWithSource {
|
||||
private _parseBinding(
|
||||
value: string, isHostBinding: boolean, sourceSpan: ParseSourceSpan,
|
||||
absoluteOffset: number): ASTWithSource {
|
||||
const sourceInfo = (sourceSpan && sourceSpan.start || '(unknown)').toString();
|
||||
|
||||
try {
|
||||
const ast = isHostBinding ?
|
||||
this._exprParser.parseSimpleBinding(value, sourceInfo, this._interpolationConfig) :
|
||||
this._exprParser.parseBinding(value, sourceInfo, this._interpolationConfig);
|
||||
this._exprParser.parseSimpleBinding(
|
||||
value, sourceInfo, absoluteOffset, this._interpolationConfig) :
|
||||
this._exprParser.parseBinding(
|
||||
value, sourceInfo, absoluteOffset, this._interpolationConfig);
|
||||
if (ast) this._reportExpressionParserErrors(ast.errors, sourceSpan);
|
||||
this._checkPipes(ast, sourceSpan);
|
||||
return ast;
|
||||
} catch (e) {
|
||||
this._reportError(`${e}`, sourceSpan);
|
||||
return this._exprParser.wrapLiteralPrimitive('ERROR', sourceInfo);
|
||||
return this._exprParser.wrapLiteralPrimitive('ERROR', sourceInfo, absoluteOffset);
|
||||
}
|
||||
}
|
||||
|
||||
@ -362,21 +370,23 @@ export class BindingParser {
|
||||
|
||||
private _parseAction(value: string, sourceSpan: ParseSourceSpan): ASTWithSource {
|
||||
const sourceInfo = (sourceSpan && sourceSpan.start || '(unknown').toString();
|
||||
const absoluteOffset = (sourceSpan && sourceSpan.start) ? sourceSpan.start.offset : 0;
|
||||
|
||||
try {
|
||||
const ast = this._exprParser.parseAction(value, sourceInfo, this._interpolationConfig);
|
||||
const ast = this._exprParser.parseAction(
|
||||
value, sourceInfo, absoluteOffset, this._interpolationConfig);
|
||||
if (ast) {
|
||||
this._reportExpressionParserErrors(ast.errors, sourceSpan);
|
||||
}
|
||||
if (!ast || ast.ast instanceof EmptyExpr) {
|
||||
this._reportError(`Empty expressions are not allowed`, sourceSpan);
|
||||
return this._exprParser.wrapLiteralPrimitive('ERROR', sourceInfo);
|
||||
return this._exprParser.wrapLiteralPrimitive('ERROR', sourceInfo, absoluteOffset);
|
||||
}
|
||||
this._checkPipes(ast, sourceSpan);
|
||||
return ast;
|
||||
} catch (e) {
|
||||
this._reportError(`${e}`, sourceSpan);
|
||||
return this._exprParser.wrapLiteralPrimitive('ERROR', sourceInfo);
|
||||
return this._exprParser.wrapLiteralPrimitive('ERROR', sourceInfo, absoluteOffset);
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user