fix(compiler): ensure that event handlers have the correct source spans (#28055)
When template bindings are being parsed the event handlers were receiving a source span that included the whole attribute. Now they get a span that is focussed on the handler itself. PR Close #28055
This commit is contained in:
parent
497619f25d
commit
cffd86260a
@ -698,7 +698,8 @@ export class ParsedEvent {
|
|||||||
// Animation events have a phase
|
// Animation events have a phase
|
||||||
constructor(
|
constructor(
|
||||||
public name: string, public targetOrPhase: string, public type: ParsedEventType,
|
public name: string, public targetOrPhase: string, public type: ParsedEventType,
|
||||||
public handler: AST, public sourceSpan: ParseSourceSpan) {}
|
public handler: AST, public sourceSpan: ParseSourceSpan,
|
||||||
|
public handlerSpan: ParseSourceSpan) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ParsedVariable {
|
export class ParsedVariable {
|
||||||
|
@ -50,13 +50,15 @@ export class BoundAttribute implements Node {
|
|||||||
export class BoundEvent implements Node {
|
export class BoundEvent implements Node {
|
||||||
constructor(
|
constructor(
|
||||||
public name: string, public type: ParsedEventType, public handler: AST,
|
public name: string, public type: ParsedEventType, public handler: AST,
|
||||||
public target: string|null, public phase: string|null, public sourceSpan: ParseSourceSpan) {}
|
public target: string|null, public phase: string|null, public sourceSpan: ParseSourceSpan,
|
||||||
|
public handlerSpan: ParseSourceSpan) {}
|
||||||
|
|
||||||
static fromParsedEvent(event: ParsedEvent) {
|
static fromParsedEvent(event: ParsedEvent) {
|
||||||
const target: string|null = event.type === ParsedEventType.Regular ? event.targetOrPhase : null;
|
const target: string|null = event.type === ParsedEventType.Regular ? event.targetOrPhase : null;
|
||||||
const phase: string|null =
|
const phase: string|null =
|
||||||
event.type === ParsedEventType.Animation ? event.targetOrPhase : null;
|
event.type === ParsedEventType.Animation ? event.targetOrPhase : null;
|
||||||
return new BoundEvent(event.name, event.type, event.handler, target, phase, event.sourceSpan);
|
return new BoundEvent(
|
||||||
|
event.name, event.type, event.handler, target, phase, event.sourceSpan, event.handlerSpan);
|
||||||
}
|
}
|
||||||
|
|
||||||
visit<Result>(visitor: Visitor<Result>): Result { return visitor.visitBoundEvent(this); }
|
visit<Result>(visitor: Visitor<Result>): Result { return visitor.visitBoundEvent(this); }
|
||||||
@ -67,7 +69,12 @@ export class Element implements Node {
|
|||||||
public name: string, public attributes: TextAttribute[], public inputs: BoundAttribute[],
|
public name: string, public attributes: TextAttribute[], public inputs: BoundAttribute[],
|
||||||
public outputs: BoundEvent[], public children: Node[], public references: Reference[],
|
public outputs: BoundEvent[], public children: Node[], public references: Reference[],
|
||||||
public sourceSpan: ParseSourceSpan, public startSourceSpan: ParseSourceSpan|null,
|
public sourceSpan: ParseSourceSpan, public startSourceSpan: ParseSourceSpan|null,
|
||||||
public endSourceSpan: ParseSourceSpan|null, public i18n?: I18nAST) {}
|
public endSourceSpan: ParseSourceSpan|null, public i18n?: I18nAST) {
|
||||||
|
// If the element is empty then the source span should include any closing tag
|
||||||
|
if (children.length === 0 && startSourceSpan && endSourceSpan) {
|
||||||
|
this.sourceSpan = {...sourceSpan, end: endSourceSpan.end};
|
||||||
|
}
|
||||||
|
}
|
||||||
visit<Result>(visitor: Visitor<Result>): Result { return visitor.visitElement(this); }
|
visit<Result>(visitor: Visitor<Result>): Result { return visitor.visitElement(this); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -284,13 +284,15 @@ class HtmlAstToIvyAst implements html.Visitor {
|
|||||||
} else if (bindParts[KW_ON_IDX]) {
|
} else if (bindParts[KW_ON_IDX]) {
|
||||||
const events: ParsedEvent[] = [];
|
const events: ParsedEvent[] = [];
|
||||||
this.bindingParser.parseEvent(
|
this.bindingParser.parseEvent(
|
||||||
bindParts[IDENT_KW_IDX], value, srcSpan, matchableAttributes, events);
|
bindParts[IDENT_KW_IDX], value, srcSpan, attribute.valueSpan || srcSpan,
|
||||||
|
matchableAttributes, events);
|
||||||
addEvents(events, boundEvents);
|
addEvents(events, boundEvents);
|
||||||
} else if (bindParts[KW_BINDON_IDX]) {
|
} else if (bindParts[KW_BINDON_IDX]) {
|
||||||
this.bindingParser.parsePropertyBinding(
|
this.bindingParser.parsePropertyBinding(
|
||||||
bindParts[IDENT_KW_IDX], value, false, srcSpan, matchableAttributes, parsedProperties);
|
bindParts[IDENT_KW_IDX], value, false, srcSpan, matchableAttributes, parsedProperties);
|
||||||
this.parseAssignmentEvent(
|
this.parseAssignmentEvent(
|
||||||
bindParts[IDENT_KW_IDX], value, srcSpan, matchableAttributes, boundEvents);
|
bindParts[IDENT_KW_IDX], value, srcSpan, attribute.valueSpan, matchableAttributes,
|
||||||
|
boundEvents);
|
||||||
} else if (bindParts[KW_AT_IDX]) {
|
} else if (bindParts[KW_AT_IDX]) {
|
||||||
this.bindingParser.parseLiteralAttr(
|
this.bindingParser.parseLiteralAttr(
|
||||||
name, value, srcSpan, matchableAttributes, parsedProperties);
|
name, value, srcSpan, matchableAttributes, parsedProperties);
|
||||||
@ -300,7 +302,8 @@ class HtmlAstToIvyAst implements html.Visitor {
|
|||||||
bindParts[IDENT_BANANA_BOX_IDX], value, false, srcSpan, matchableAttributes,
|
bindParts[IDENT_BANANA_BOX_IDX], value, false, srcSpan, matchableAttributes,
|
||||||
parsedProperties);
|
parsedProperties);
|
||||||
this.parseAssignmentEvent(
|
this.parseAssignmentEvent(
|
||||||
bindParts[IDENT_BANANA_BOX_IDX], value, srcSpan, matchableAttributes, boundEvents);
|
bindParts[IDENT_BANANA_BOX_IDX], value, srcSpan, attribute.valueSpan,
|
||||||
|
matchableAttributes, boundEvents);
|
||||||
|
|
||||||
} else if (bindParts[IDENT_PROPERTY_IDX]) {
|
} else if (bindParts[IDENT_PROPERTY_IDX]) {
|
||||||
this.bindingParser.parsePropertyBinding(
|
this.bindingParser.parsePropertyBinding(
|
||||||
@ -310,7 +313,8 @@ class HtmlAstToIvyAst implements html.Visitor {
|
|||||||
} else if (bindParts[IDENT_EVENT_IDX]) {
|
} else if (bindParts[IDENT_EVENT_IDX]) {
|
||||||
const events: ParsedEvent[] = [];
|
const events: ParsedEvent[] = [];
|
||||||
this.bindingParser.parseEvent(
|
this.bindingParser.parseEvent(
|
||||||
bindParts[IDENT_EVENT_IDX], value, srcSpan, matchableAttributes, events);
|
bindParts[IDENT_EVENT_IDX], value, srcSpan, attribute.valueSpan || srcSpan,
|
||||||
|
matchableAttributes, events);
|
||||||
addEvents(events, boundEvents);
|
addEvents(events, boundEvents);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -347,10 +351,12 @@ class HtmlAstToIvyAst implements html.Visitor {
|
|||||||
|
|
||||||
private parseAssignmentEvent(
|
private parseAssignmentEvent(
|
||||||
name: string, expression: string, sourceSpan: ParseSourceSpan,
|
name: string, expression: string, sourceSpan: ParseSourceSpan,
|
||||||
targetMatchableAttrs: string[][], boundEvents: t.BoundEvent[]) {
|
valueSpan: ParseSourceSpan|undefined, targetMatchableAttrs: string[][],
|
||||||
|
boundEvents: t.BoundEvent[]) {
|
||||||
const events: ParsedEvent[] = [];
|
const events: ParsedEvent[] = [];
|
||||||
this.bindingParser.parseEvent(
|
this.bindingParser.parseEvent(
|
||||||
`${name}Change`, `${expression}=$event`, sourceSpan, targetMatchableAttrs, events);
|
`${name}Change`, `${expression}=$event`, sourceSpan, valueSpan || sourceSpan,
|
||||||
|
targetMatchableAttrs, events);
|
||||||
addEvents(events, boundEvents);
|
addEvents(events, boundEvents);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,7 +79,8 @@ export function prepareEventListenerParameters(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const bindingExpr = convertActionBinding(
|
const bindingExpr = convertActionBinding(
|
||||||
scope, bindingContext, handler, 'b', () => error('Unexpected interpolation'));
|
scope, bindingContext, handler, 'b', () => error('Unexpected interpolation'),
|
||||||
|
eventAst.handlerSpan);
|
||||||
|
|
||||||
const statements = [];
|
const statements = [];
|
||||||
if (scope) {
|
if (scope) {
|
||||||
|
@ -83,7 +83,8 @@ export class BindingParser {
|
|||||||
Object.keys(dirMeta.hostListeners).forEach(propName => {
|
Object.keys(dirMeta.hostListeners).forEach(propName => {
|
||||||
const expression = dirMeta.hostListeners[propName];
|
const expression = dirMeta.hostListeners[propName];
|
||||||
if (typeof expression === 'string') {
|
if (typeof expression === 'string') {
|
||||||
this.parseEvent(propName, expression, sourceSpan, [], targetEvents);
|
// TODO: pass a more accurate handlerSpan for this event.
|
||||||
|
this.parseEvent(propName, expression, sourceSpan, sourceSpan, [], targetEvents);
|
||||||
} else {
|
} else {
|
||||||
this._reportError(
|
this._reportError(
|
||||||
`Value of the host listener "${propName}" needs to be a string representing an expression but got "${expression}" (${typeof expression})`,
|
`Value of the host listener "${propName}" needs to be a string representing an expression but got "${expression}" (${typeof expression})`,
|
||||||
@ -300,13 +301,14 @@ export class BindingParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
parseEvent(
|
parseEvent(
|
||||||
name: string, expression: string, sourceSpan: ParseSourceSpan,
|
name: string, expression: string, sourceSpan: ParseSourceSpan, handlerSpan: ParseSourceSpan,
|
||||||
targetMatchableAttrs: string[][], targetEvents: ParsedEvent[]) {
|
targetMatchableAttrs: string[][], targetEvents: ParsedEvent[]) {
|
||||||
if (isAnimationLabel(name)) {
|
if (isAnimationLabel(name)) {
|
||||||
name = name.substr(1);
|
name = name.substr(1);
|
||||||
this._parseAnimationEvent(name, expression, sourceSpan, targetEvents);
|
this._parseAnimationEvent(name, expression, sourceSpan, handlerSpan, targetEvents);
|
||||||
} else {
|
} else {
|
||||||
this._parseRegularEvent(name, expression, sourceSpan, targetMatchableAttrs, targetEvents);
|
this._parseRegularEvent(
|
||||||
|
name, expression, sourceSpan, handlerSpan, targetMatchableAttrs, targetEvents);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -317,7 +319,8 @@ export class BindingParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _parseAnimationEvent(
|
private _parseAnimationEvent(
|
||||||
name: string, expression: string, sourceSpan: ParseSourceSpan, targetEvents: ParsedEvent[]) {
|
name: string, expression: string, sourceSpan: ParseSourceSpan, handlerSpan: ParseSourceSpan,
|
||||||
|
targetEvents: ParsedEvent[]) {
|
||||||
const matches = splitAtPeriod(name, [name, '']);
|
const matches = splitAtPeriod(name, [name, '']);
|
||||||
const eventName = matches[0];
|
const eventName = matches[0];
|
||||||
const phase = matches[1].toLowerCase();
|
const phase = matches[1].toLowerCase();
|
||||||
@ -325,9 +328,9 @@ export class BindingParser {
|
|||||||
switch (phase) {
|
switch (phase) {
|
||||||
case 'start':
|
case 'start':
|
||||||
case 'done':
|
case 'done':
|
||||||
const ast = this._parseAction(expression, sourceSpan);
|
const ast = this._parseAction(expression, handlerSpan);
|
||||||
targetEvents.push(
|
targetEvents.push(new ParsedEvent(
|
||||||
new ParsedEvent(eventName, phase, ParsedEventType.Animation, ast, sourceSpan));
|
eventName, phase, ParsedEventType.Animation, ast, sourceSpan, handlerSpan));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@ -344,13 +347,14 @@ export class BindingParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _parseRegularEvent(
|
private _parseRegularEvent(
|
||||||
name: string, expression: string, sourceSpan: ParseSourceSpan,
|
name: string, expression: string, sourceSpan: ParseSourceSpan, handlerSpan: ParseSourceSpan,
|
||||||
targetMatchableAttrs: string[][], targetEvents: ParsedEvent[]) {
|
targetMatchableAttrs: string[][], targetEvents: ParsedEvent[]) {
|
||||||
// long format: 'target: eventName'
|
// long format: 'target: eventName'
|
||||||
const [target, eventName] = splitAtColon(name, [null !, name]);
|
const [target, eventName] = splitAtColon(name, [null !, name]);
|
||||||
const ast = this._parseAction(expression, sourceSpan);
|
const ast = this._parseAction(expression, handlerSpan);
|
||||||
targetMatchableAttrs.push([name !, ast.source !]);
|
targetMatchableAttrs.push([name !, ast.source !]);
|
||||||
targetEvents.push(new ParsedEvent(eventName, target, ParsedEventType.Regular, ast, sourceSpan));
|
targetEvents.push(
|
||||||
|
new ParsedEvent(eventName, target, ParsedEventType.Regular, ast, sourceSpan, handlerSpan));
|
||||||
// Don't detect directives for event names for now,
|
// Don't detect directives for event names for now,
|
||||||
// so don't add the event name to the matchableAttrs
|
// so don't add the event name to the matchableAttrs
|
||||||
}
|
}
|
||||||
|
@ -114,7 +114,8 @@ export class BoundEventAst implements TemplateAst {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public name: string, public target: string|null, public phase: string|null,
|
public name: string, public target: string|null, public phase: string|null,
|
||||||
public handler: AST, public sourceSpan: ParseSourceSpan) {
|
public handler: AST, public sourceSpan: ParseSourceSpan,
|
||||||
|
public handlerSpan: ParseSourceSpan) {
|
||||||
this.fullName = BoundEventAst.calcFullName(this.name, this.target, this.phase);
|
this.fullName = BoundEventAst.calcFullName(this.name, this.target, this.phase);
|
||||||
this.isAnimation = !!this.phase;
|
this.isAnimation = !!this.phase;
|
||||||
}
|
}
|
||||||
@ -134,7 +135,8 @@ export class BoundEventAst implements TemplateAst {
|
|||||||
const target: string|null = event.type === ParsedEventType.Regular ? event.targetOrPhase : null;
|
const target: string|null = event.type === ParsedEventType.Regular ? event.targetOrPhase : null;
|
||||||
const phase: string|null =
|
const phase: string|null =
|
||||||
event.type === ParsedEventType.Animation ? event.targetOrPhase : null;
|
event.type === ParsedEventType.Animation ? event.targetOrPhase : null;
|
||||||
return new BoundEventAst(event.name, target, phase, event.handler, event.sourceSpan);
|
return new BoundEventAst(
|
||||||
|
event.name, target, phase, event.handler, event.sourceSpan, event.handlerSpan);
|
||||||
}
|
}
|
||||||
|
|
||||||
visit(visitor: TemplateAstVisitor, context: any): any {
|
visit(visitor: TemplateAstVisitor, context: any): any {
|
||||||
|
@ -441,13 +441,15 @@ class TemplateParseVisitor implements html.Visitor {
|
|||||||
|
|
||||||
} else if (bindParts[KW_ON_IDX]) {
|
} else if (bindParts[KW_ON_IDX]) {
|
||||||
this._bindingParser.parseEvent(
|
this._bindingParser.parseEvent(
|
||||||
bindParts[IDENT_KW_IDX], value, srcSpan, targetMatchableAttrs, boundEvents);
|
bindParts[IDENT_KW_IDX], value, srcSpan, attr.valueSpan || srcSpan,
|
||||||
|
targetMatchableAttrs, boundEvents);
|
||||||
|
|
||||||
} else if (bindParts[KW_BINDON_IDX]) {
|
} else if (bindParts[KW_BINDON_IDX]) {
|
||||||
this._bindingParser.parsePropertyBinding(
|
this._bindingParser.parsePropertyBinding(
|
||||||
bindParts[IDENT_KW_IDX], value, false, srcSpan, targetMatchableAttrs, targetProps);
|
bindParts[IDENT_KW_IDX], value, false, srcSpan, targetMatchableAttrs, targetProps);
|
||||||
this._parseAssignmentEvent(
|
this._parseAssignmentEvent(
|
||||||
bindParts[IDENT_KW_IDX], value, srcSpan, targetMatchableAttrs, boundEvents);
|
bindParts[IDENT_KW_IDX], value, srcSpan, attr.valueSpan || srcSpan,
|
||||||
|
targetMatchableAttrs, boundEvents);
|
||||||
|
|
||||||
} else if (bindParts[KW_AT_IDX]) {
|
} else if (bindParts[KW_AT_IDX]) {
|
||||||
this._bindingParser.parseLiteralAttr(
|
this._bindingParser.parseLiteralAttr(
|
||||||
@ -458,7 +460,8 @@ class TemplateParseVisitor implements html.Visitor {
|
|||||||
bindParts[IDENT_BANANA_BOX_IDX], value, false, srcSpan, targetMatchableAttrs,
|
bindParts[IDENT_BANANA_BOX_IDX], value, false, srcSpan, targetMatchableAttrs,
|
||||||
targetProps);
|
targetProps);
|
||||||
this._parseAssignmentEvent(
|
this._parseAssignmentEvent(
|
||||||
bindParts[IDENT_BANANA_BOX_IDX], value, srcSpan, targetMatchableAttrs, boundEvents);
|
bindParts[IDENT_BANANA_BOX_IDX], value, srcSpan, attr.valueSpan || srcSpan,
|
||||||
|
targetMatchableAttrs, boundEvents);
|
||||||
|
|
||||||
} else if (bindParts[IDENT_PROPERTY_IDX]) {
|
} else if (bindParts[IDENT_PROPERTY_IDX]) {
|
||||||
this._bindingParser.parsePropertyBinding(
|
this._bindingParser.parsePropertyBinding(
|
||||||
@ -467,7 +470,8 @@ class TemplateParseVisitor implements html.Visitor {
|
|||||||
|
|
||||||
} else if (bindParts[IDENT_EVENT_IDX]) {
|
} else if (bindParts[IDENT_EVENT_IDX]) {
|
||||||
this._bindingParser.parseEvent(
|
this._bindingParser.parseEvent(
|
||||||
bindParts[IDENT_EVENT_IDX], value, srcSpan, targetMatchableAttrs, boundEvents);
|
bindParts[IDENT_EVENT_IDX], value, srcSpan, attr.valueSpan || srcSpan,
|
||||||
|
targetMatchableAttrs, boundEvents);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
hasBinding = this._bindingParser.parsePropertyInterpolation(
|
hasBinding = this._bindingParser.parsePropertyInterpolation(
|
||||||
@ -507,10 +511,11 @@ class TemplateParseVisitor implements html.Visitor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _parseAssignmentEvent(
|
private _parseAssignmentEvent(
|
||||||
name: string, expression: string, sourceSpan: ParseSourceSpan,
|
name: string, expression: string, sourceSpan: ParseSourceSpan, valueSpan: ParseSourceSpan,
|
||||||
targetMatchableAttrs: string[][], targetEvents: ParsedEvent[]) {
|
targetMatchableAttrs: string[][], targetEvents: ParsedEvent[]) {
|
||||||
this._bindingParser.parseEvent(
|
this._bindingParser.parseEvent(
|
||||||
`${name}Change`, `${expression}=$event`, sourceSpan, targetMatchableAttrs, targetEvents);
|
`${name}Change`, `${expression}=$event`, sourceSpan, valueSpan, targetMatchableAttrs,
|
||||||
|
targetEvents);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _parseDirectives(selectorMatcher: SelectorMatcher, elementCssSelector: CssSelector):
|
private _parseDirectives(selectorMatcher: SelectorMatcher, elementCssSelector: CssSelector):
|
||||||
|
@ -420,7 +420,7 @@ class ArrayConsole implements Console {
|
|||||||
expectVisitedNode(
|
expectVisitedNode(
|
||||||
new class extends
|
new class extends
|
||||||
NullVisitor{visitEvent(ast: BoundEventAst, context: any): any{return ast;}},
|
NullVisitor{visitEvent(ast: BoundEventAst, context: any): any{return ast;}},
|
||||||
new BoundEventAst('foo', 'bar', 'goo', null !, null !));
|
new BoundEventAst('foo', 'bar', 'goo', null !, null !, null !));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should visit BoundElementPropertyAst', () => {
|
it('should visit BoundElementPropertyAst', () => {
|
||||||
@ -474,7 +474,7 @@ class ArrayConsole implements Console {
|
|||||||
new EmbeddedTemplateAst([], [], [], [], [], [], false, [], [], 0, null !),
|
new EmbeddedTemplateAst([], [], [], [], [], [], false, [], [], 0, null !),
|
||||||
new ElementAst('foo', [], [], [], [], [], [], false, [], [], 0, null !, null !),
|
new ElementAst('foo', [], [], [], [], [], [], false, [], [], 0, null !, null !),
|
||||||
new ReferenceAst('foo', null !, 'bar', null !), new VariableAst('foo', 'bar', null !),
|
new ReferenceAst('foo', null !, 'bar', null !), new VariableAst('foo', 'bar', null !),
|
||||||
new BoundEventAst('foo', 'bar', 'goo', null !, null !),
|
new BoundEventAst('foo', 'bar', 'goo', null !, null !, null !),
|
||||||
new BoundElementPropertyAst('foo', null !, null !, null !, 'bar', null !),
|
new BoundElementPropertyAst('foo', null !, null !, null !, 'bar', null !),
|
||||||
new AttrAst('foo', 'bar', null !), new BoundTextAst(null !, 0, null !),
|
new AttrAst('foo', 'bar', null !), new BoundTextAst(null !, 0, null !),
|
||||||
new TextAst('foo', 0, null !), new DirectiveAst(null !, [], [], [], 0, null !),
|
new TextAst('foo', 0, null !), new DirectiveAst(null !, [], [], [], 0, null !),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user