feat(ivy): include value spans for attributes, variables and references (#30181)

Template AST nodes for (bound) attributes, variables and references will
now retain a reference to the source span of their value, which allows
for more accurate type check diagnostics.

PR Close #30181
This commit is contained in:
JoostK
2019-05-04 22:41:17 +02:00
committed by Miško Hevery
parent 985513351b
commit 489cef6ea2
8 changed files with 395 additions and 61 deletions

View File

@ -691,7 +691,7 @@ export class ParsedProperty {
constructor(
public name: string, public expression: ASTWithSource, public type: ParsedPropertyType,
public sourceSpan: ParseSourceSpan) {
public sourceSpan: ParseSourceSpan, public valueSpan?: ParseSourceSpan) {
this.isLiteral = this.type === ParsedPropertyType.LITERAL_ATTR;
this.isAnimation = this.type === ParsedPropertyType.ANIMATION;
}
@ -739,5 +739,6 @@ export const enum BindingType {
export class BoundElementProperty {
constructor(
public name: string, public type: BindingType, public securityContext: SecurityContext,
public value: AST, public unit: string|null, public sourceSpan: ParseSourceSpan) {}
public value: AST, public unit: string|null, public sourceSpan: ParseSourceSpan,
public valueSpan?: ParseSourceSpan) {}
}

View File

@ -37,11 +37,12 @@ export class BoundAttribute implements Node {
constructor(
public name: string, public type: BindingType, public securityContext: SecurityContext,
public value: AST, public unit: string|null, public sourceSpan: ParseSourceSpan,
public i18n?: I18nAST) {}
public valueSpan?: ParseSourceSpan, public i18n?: I18nAST) {}
static fromBoundElementProperty(prop: BoundElementProperty, i18n?: I18nAST) {
return new BoundAttribute(
prop.name, prop.type, prop.securityContext, prop.value, prop.unit, prop.sourceSpan, i18n);
prop.name, prop.type, prop.securityContext, prop.value, prop.unit, prop.sourceSpan,
prop.valueSpan, i18n);
}
visit<Result>(visitor: Visitor<Result>): Result { return visitor.visitBoundAttribute(this); }
@ -96,12 +97,16 @@ export class Content implements Node {
}
export class Variable implements Node {
constructor(public name: string, public value: string, public sourceSpan: ParseSourceSpan) {}
constructor(
public name: string, public value: string, public sourceSpan: ParseSourceSpan,
public valueSpan?: ParseSourceSpan) {}
visit<Result>(visitor: Visitor<Result>): Result { return visitor.visitVariable(this); }
}
export class Reference implements Node {
constructor(public name: string, public value: string, public sourceSpan: ParseSourceSpan) {}
constructor(
public name: string, public value: string, public sourceSpan: ParseSourceSpan,
public valueSpan?: ParseSourceSpan) {}
visit<Result>(visitor: Visitor<Result>): Result { return visitor.visitReference(this); }
}

View File

@ -297,20 +297,20 @@ class HtmlAstToIvyAst implements html.Visitor {
hasBinding = true;
if (bindParts[KW_BIND_IDX] != null) {
this.bindingParser.parsePropertyBinding(
bindParts[IDENT_KW_IDX], value, false, srcSpan, absoluteOffset, matchableAttributes,
parsedProperties);
bindParts[IDENT_KW_IDX], value, false, srcSpan, absoluteOffset, attribute.valueSpan,
matchableAttributes, parsedProperties);
} else if (bindParts[KW_LET_IDX]) {
if (isTemplateElement) {
const identifier = bindParts[IDENT_KW_IDX];
this.parseVariable(identifier, value, srcSpan, variables);
this.parseVariable(identifier, value, srcSpan, attribute.valueSpan, variables);
} else {
this.reportError(`"let-" is only supported on ng-template elements.`, srcSpan);
}
} else if (bindParts[KW_REF_IDX]) {
const identifier = bindParts[IDENT_KW_IDX];
this.parseReference(identifier, value, srcSpan, references);
this.parseReference(identifier, value, srcSpan, attribute.valueSpan, references);
} else if (bindParts[KW_ON_IDX]) {
const events: ParsedEvent[] = [];
@ -320,19 +320,20 @@ class HtmlAstToIvyAst implements html.Visitor {
addEvents(events, boundEvents);
} else if (bindParts[KW_BINDON_IDX]) {
this.bindingParser.parsePropertyBinding(
bindParts[IDENT_KW_IDX], value, false, srcSpan, absoluteOffset, matchableAttributes,
parsedProperties);
bindParts[IDENT_KW_IDX], value, false, srcSpan, absoluteOffset, attribute.valueSpan,
matchableAttributes, parsedProperties);
this.parseAssignmentEvent(
bindParts[IDENT_KW_IDX], value, srcSpan, attribute.valueSpan, matchableAttributes,
boundEvents);
} else if (bindParts[KW_AT_IDX]) {
this.bindingParser.parseLiteralAttr(
name, value, srcSpan, absoluteOffset, matchableAttributes, parsedProperties);
name, value, srcSpan, absoluteOffset, attribute.valueSpan, matchableAttributes,
parsedProperties);
} else if (bindParts[IDENT_BANANA_BOX_IDX]) {
this.bindingParser.parsePropertyBinding(
bindParts[IDENT_BANANA_BOX_IDX], value, false, srcSpan, absoluteOffset,
matchableAttributes, parsedProperties);
attribute.valueSpan, matchableAttributes, parsedProperties);
this.parseAssignmentEvent(
bindParts[IDENT_BANANA_BOX_IDX], value, srcSpan, attribute.valueSpan,
matchableAttributes, boundEvents);
@ -340,7 +341,7 @@ class HtmlAstToIvyAst implements html.Visitor {
} else if (bindParts[IDENT_PROPERTY_IDX]) {
this.bindingParser.parsePropertyBinding(
bindParts[IDENT_PROPERTY_IDX], value, false, srcSpan, absoluteOffset,
matchableAttributes, parsedProperties);
attribute.valueSpan, matchableAttributes, parsedProperties);
} else if (bindParts[IDENT_EVENT_IDX]) {
const events: ParsedEvent[] = [];
@ -351,7 +352,7 @@ class HtmlAstToIvyAst implements html.Visitor {
}
} else {
hasBinding = this.bindingParser.parsePropertyInterpolation(
name, value, srcSpan, matchableAttributes, parsedProperties);
name, value, srcSpan, attribute.valueSpan, matchableAttributes, parsedProperties);
}
return hasBinding;
@ -365,20 +366,22 @@ class HtmlAstToIvyAst implements html.Visitor {
}
private parseVariable(
identifier: string, value: string, sourceSpan: ParseSourceSpan, variables: t.Variable[]) {
identifier: string, value: string, sourceSpan: ParseSourceSpan,
valueSpan: ParseSourceSpan|undefined, variables: t.Variable[]) {
if (identifier.indexOf('-') > -1) {
this.reportError(`"-" is not allowed in variable names`, sourceSpan);
}
variables.push(new t.Variable(identifier, value, sourceSpan));
variables.push(new t.Variable(identifier, value, sourceSpan, valueSpan));
}
private parseReference(
identifier: string, value: string, sourceSpan: ParseSourceSpan, references: t.Reference[]) {
identifier: string, value: string, sourceSpan: ParseSourceSpan,
valueSpan: ParseSourceSpan|undefined, references: t.Reference[]) {
if (identifier.indexOf('-') > -1) {
this.reportError(`"-" is not allowed in reference names`, sourceSpan);
}
references.push(new t.Reference(identifier, value, sourceSpan));
references.push(new t.Reference(identifier, value, sourceSpan, valueSpan));
}
private parseAssignmentEvent(

View File

@ -57,7 +57,8 @@ export class BindingParser {
const expression = dirMeta.hostProperties[propName];
if (typeof expression === 'string') {
this.parsePropertyBinding(
propName, expression, true, sourceSpan, sourceSpan.start.offset, [], boundProps);
propName, expression, true, sourceSpan, sourceSpan.start.offset, undefined, [],
boundProps);
} else {
this._reportError(
`Value of the host property binding "${propName}" needs to be a string representing an expression but got "${expression}" (${typeof expression})`,
@ -125,11 +126,13 @@ export class BindingParser {
targetVars.push(new ParsedVariable(binding.key, binding.name, sourceSpan));
} else if (binding.expression) {
this._parsePropertyAst(
binding.key, binding.expression, sourceSpan, targetMatchableAttrs, targetProps);
binding.key, binding.expression, sourceSpan, undefined, targetMatchableAttrs,
targetProps);
} else {
targetMatchableAttrs.push([binding.key, '']);
this.parseLiteralAttr(
binding.key, null, sourceSpan, absoluteOffset, targetMatchableAttrs, targetProps);
binding.key, null, sourceSpan, absoluteOffset, undefined, targetMatchableAttrs,
targetProps);
}
}
}
@ -158,7 +161,8 @@ export class BindingParser {
parseLiteralAttr(
name: string, value: string|null, sourceSpan: ParseSourceSpan, absoluteOffset: number,
targetMatchableAttrs: string[][], targetProps: ParsedProperty[]) {
valueSpan: ParseSourceSpan|undefined, targetMatchableAttrs: string[][],
targetProps: ParsedProperty[]) {
if (isAnimationLabel(name)) {
name = name.substring(1);
if (value) {
@ -168,17 +172,18 @@ export class BindingParser {
sourceSpan, ParseErrorLevel.ERROR);
}
this._parseAnimation(
name, value, sourceSpan, absoluteOffset, targetMatchableAttrs, targetProps);
name, value, sourceSpan, absoluteOffset, valueSpan, targetMatchableAttrs, targetProps);
} else {
targetProps.push(new ParsedProperty(
name, this._exprParser.wrapLiteralPrimitive(value, '', absoluteOffset),
ParsedPropertyType.LITERAL_ATTR, sourceSpan));
ParsedPropertyType.LITERAL_ATTR, sourceSpan, valueSpan));
}
}
parsePropertyBinding(
name: string, expression: string, isHost: boolean, sourceSpan: ParseSourceSpan,
absoluteOffset: number, targetMatchableAttrs: string[][], targetProps: ParsedProperty[]) {
absoluteOffset: number, valueSpan: ParseSourceSpan|undefined,
targetMatchableAttrs: string[][], targetProps: ParsedProperty[]) {
let isAnimationProp = false;
if (name.startsWith(ANIMATE_PROP_PREFIX)) {
isAnimationProp = true;
@ -190,20 +195,22 @@ export class BindingParser {
if (isAnimationProp) {
this._parseAnimation(
name, expression, sourceSpan, absoluteOffset, targetMatchableAttrs, targetProps);
name, expression, sourceSpan, absoluteOffset, valueSpan, targetMatchableAttrs,
targetProps);
} else {
this._parsePropertyAst(
name, this._parseBinding(expression, isHost, sourceSpan, absoluteOffset), sourceSpan,
targetMatchableAttrs, targetProps);
name, this._parseBinding(expression, isHost, valueSpan || sourceSpan, absoluteOffset),
sourceSpan, valueSpan, targetMatchableAttrs, targetProps);
}
}
parsePropertyInterpolation(
name: string, value: string, sourceSpan: ParseSourceSpan, targetMatchableAttrs: string[][],
name: string, value: string, sourceSpan: ParseSourceSpan,
valueSpan: ParseSourceSpan|undefined, targetMatchableAttrs: string[][],
targetProps: ParsedProperty[]): boolean {
const expr = this.parseInterpolation(value, sourceSpan);
const expr = this.parseInterpolation(value, valueSpan || sourceSpan);
if (expr) {
this._parsePropertyAst(name, expr, sourceSpan, targetMatchableAttrs, targetProps);
this._parsePropertyAst(name, expr, sourceSpan, valueSpan, targetMatchableAttrs, targetProps);
return true;
}
return false;
@ -211,20 +218,25 @@ export class BindingParser {
private _parsePropertyAst(
name: string, ast: ASTWithSource, sourceSpan: ParseSourceSpan,
targetMatchableAttrs: string[][], targetProps: ParsedProperty[]) {
valueSpan: ParseSourceSpan|undefined, targetMatchableAttrs: string[][],
targetProps: ParsedProperty[]) {
targetMatchableAttrs.push([name, ast.source !]);
targetProps.push(new ParsedProperty(name, ast, ParsedPropertyType.DEFAULT, sourceSpan));
targetProps.push(
new ParsedProperty(name, ast, ParsedPropertyType.DEFAULT, sourceSpan, valueSpan));
}
private _parseAnimation(
name: string, expression: string|null, sourceSpan: ParseSourceSpan, absoluteOffset: number,
targetMatchableAttrs: string[][], targetProps: ParsedProperty[]) {
valueSpan: ParseSourceSpan|undefined, 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, absoluteOffset);
const ast = this._parseBinding(
expression || 'undefined', false, valueSpan || sourceSpan, absoluteOffset);
targetMatchableAttrs.push([name, ast.source !]);
targetProps.push(new ParsedProperty(name, ast, ParsedPropertyType.ANIMATION, sourceSpan));
targetProps.push(
new ParsedProperty(name, ast, ParsedPropertyType.ANIMATION, sourceSpan, valueSpan));
}
private _parseBinding(
@ -253,7 +265,7 @@ export class BindingParser {
if (boundProp.isAnimation) {
return new BoundElementProperty(
boundProp.name, BindingType.Animation, SecurityContext.NONE, boundProp.expression, null,
boundProp.sourceSpan);
boundProp.sourceSpan, boundProp.valueSpan);
}
let unit: string|null = null;
@ -306,7 +318,7 @@ export class BindingParser {
return new BoundElementProperty(
boundPropertyName, bindingType, securityContexts[0], boundProp.expression, unit,
boundProp.sourceSpan);
boundProp.sourceSpan, boundProp.valueSpan);
}
parseEvent(

View File

@ -426,8 +426,8 @@ class TemplateParseVisitor implements html.Visitor {
hasBinding = true;
if (bindParts[KW_BIND_IDX] != null) {
this._bindingParser.parsePropertyBinding(
bindParts[IDENT_KW_IDX], value, false, srcSpan, absoluteOffset, targetMatchableAttrs,
targetProps);
bindParts[IDENT_KW_IDX], value, false, srcSpan, absoluteOffset, attr.valueSpan,
targetMatchableAttrs, targetProps);
} else if (bindParts[KW_LET_IDX]) {
if (isTemplateElement) {
@ -448,19 +448,20 @@ class TemplateParseVisitor implements html.Visitor {
} else if (bindParts[KW_BINDON_IDX]) {
this._bindingParser.parsePropertyBinding(
bindParts[IDENT_KW_IDX], value, false, srcSpan, absoluteOffset, targetMatchableAttrs,
targetProps);
bindParts[IDENT_KW_IDX], value, false, srcSpan, absoluteOffset, attr.valueSpan,
targetMatchableAttrs, targetProps);
this._parseAssignmentEvent(
bindParts[IDENT_KW_IDX], value, srcSpan, attr.valueSpan || srcSpan,
targetMatchableAttrs, boundEvents);
} else if (bindParts[KW_AT_IDX]) {
this._bindingParser.parseLiteralAttr(
name, value, srcSpan, absoluteOffset, targetMatchableAttrs, targetProps);
name, value, srcSpan, absoluteOffset, attr.valueSpan, targetMatchableAttrs,
targetProps);
} else if (bindParts[IDENT_BANANA_BOX_IDX]) {
this._bindingParser.parsePropertyBinding(
bindParts[IDENT_BANANA_BOX_IDX], value, false, srcSpan, absoluteOffset,
bindParts[IDENT_BANANA_BOX_IDX], value, false, srcSpan, absoluteOffset, attr.valueSpan,
targetMatchableAttrs, targetProps);
this._parseAssignmentEvent(
bindParts[IDENT_BANANA_BOX_IDX], value, srcSpan, attr.valueSpan || srcSpan,
@ -468,7 +469,7 @@ class TemplateParseVisitor implements html.Visitor {
} else if (bindParts[IDENT_PROPERTY_IDX]) {
this._bindingParser.parsePropertyBinding(
bindParts[IDENT_PROPERTY_IDX], value, false, srcSpan, absoluteOffset,
bindParts[IDENT_PROPERTY_IDX], value, false, srcSpan, absoluteOffset, attr.valueSpan,
targetMatchableAttrs, targetProps);
} else if (bindParts[IDENT_EVENT_IDX]) {
@ -478,12 +479,12 @@ class TemplateParseVisitor implements html.Visitor {
}
} else {
hasBinding = this._bindingParser.parsePropertyInterpolation(
name, value, srcSpan, targetMatchableAttrs, targetProps);
name, value, srcSpan, attr.valueSpan, targetMatchableAttrs, targetProps);
}
if (!hasBinding) {
this._bindingParser.parseLiteralAttr(
name, value, srcSpan, absoluteOffset, targetMatchableAttrs, targetProps);
name, value, srcSpan, absoluteOffset, attr.valueSpan, targetMatchableAttrs, targetProps);
}
targetEvents.push(...boundEvents.map(e => t.BoundEventAst.fromParsedEvent(e)));