refactor(compiler): add an `argumentSpan` to the method call AST (#41581)

This commit adds a separate span to `MethodCall` and `SafeMethodCall` which
tracks the text span between the `(` and `)` tokens of the call. Tools like
the Language Service can use this span to more accurately understand a
cursor position within a method call expression.

PR Close #41581
This commit is contained in:
Alex Rickabaugh 2021-04-12 10:12:36 -04:00 committed by Zach Arend
parent 60d023449b
commit 34545ad2cc
5 changed files with 28 additions and 10 deletions

View File

@ -705,7 +705,8 @@ class _AstToIrVisitor implements cdAst.AstVisitor {
leftMostSafe,
new cdAst.MethodCall(
leftMostSafe.span, leftMostSafe.sourceSpan, leftMostSafe.nameSpan,
leftMostSafe.receiver, leftMostSafe.name, leftMostSafe.args));
leftMostSafe.receiver, leftMostSafe.name, leftMostSafe.args,
leftMostSafe.argumentSpan));
} else {
this._nodeMap.set(
leftMostSafe,

View File

@ -308,7 +308,8 @@ export class NonNullAssert extends AST {
export class MethodCall extends ASTWithName {
constructor(
span: ParseSpan, sourceSpan: AbsoluteSourceSpan, nameSpan: AbsoluteSourceSpan,
public receiver: AST, public name: string, public args: any[]) {
public receiver: AST, public name: string, public args: any[],
public argumentSpan: AbsoluteSourceSpan) {
super(span, sourceSpan, nameSpan);
}
visit(visitor: AstVisitor, context: any = null): any {
@ -319,7 +320,8 @@ export class MethodCall extends ASTWithName {
export class SafeMethodCall extends ASTWithName {
constructor(
span: ParseSpan, sourceSpan: AbsoluteSourceSpan, nameSpan: AbsoluteSourceSpan,
public receiver: AST, public name: string, public args: any[]) {
public receiver: AST, public name: string, public args: any[],
public argumentSpan: AbsoluteSourceSpan) {
super(span, sourceSpan, nameSpan);
}
visit(visitor: AstVisitor, context: any = null): any {
@ -583,13 +585,13 @@ export class AstTransformer implements AstVisitor {
visitMethodCall(ast: MethodCall, context: any): AST {
return new MethodCall(
ast.span, ast.sourceSpan, ast.nameSpan, ast.receiver.visit(this), ast.name,
this.visitAll(ast.args));
this.visitAll(ast.args), ast.argumentSpan);
}
visitSafeMethodCall(ast: SafeMethodCall, context: any): AST {
return new SafeMethodCall(
ast.span, ast.sourceSpan, ast.nameSpan, ast.receiver.visit(this), ast.name,
this.visitAll(ast.args));
this.visitAll(ast.args), ast.argumentSpan);
}
visitFunctionCall(ast: FunctionCall, context: any): AST {
@ -719,7 +721,8 @@ export class AstMemoryEfficientTransformer implements AstVisitor {
const receiver = ast.receiver.visit(this);
const args = this.visitAll(ast.args);
if (receiver !== ast.receiver || args !== ast.args) {
return new MethodCall(ast.span, ast.sourceSpan, ast.nameSpan, receiver, ast.name, args);
return new MethodCall(
ast.span, ast.sourceSpan, ast.nameSpan, receiver, ast.name, args, ast.argumentSpan);
}
return ast;
}
@ -728,7 +731,8 @@ export class AstMemoryEfficientTransformer implements AstVisitor {
const receiver = ast.receiver.visit(this);
const args = this.visitAll(ast.args);
if (receiver !== ast.receiver || args !== ast.args) {
return new SafeMethodCall(ast.span, ast.sourceSpan, ast.nameSpan, receiver, ast.name, args);
return new SafeMethodCall(
ast.span, ast.sourceSpan, ast.nameSpan, receiver, ast.name, args, ast.argumentSpan);
}
return ast;
}

View File

@ -940,14 +940,19 @@ export class _ParseAST {
const nameSpan = this.sourceSpan(nameStart);
if (this.consumeOptionalCharacter(chars.$LPAREN)) {
const argumentStart = this.inputIndex;
this.rparensExpected++;
const args = this.parseCallArguments();
const argumentSpan =
this.span(argumentStart, this.inputIndex).toAbsolute(this.absoluteOffset);
this.expectCharacter(chars.$RPAREN);
this.rparensExpected--;
const span = this.span(start);
const sourceSpan = this.sourceSpan(start);
return isSafe ? new SafeMethodCall(span, sourceSpan, nameSpan, receiver, id, args) :
new MethodCall(span, sourceSpan, nameSpan, receiver, id, args);
return isSafe ?
new SafeMethodCall(span, sourceSpan, nameSpan, receiver, id, args, argumentSpan) :
new MethodCall(span, sourceSpan, nameSpan, receiver, id, args, argumentSpan);
} else {
if (isSafe) {

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {AbsoluteSourceSpan, ASTWithSource, BindingPipe, EmptyExpr, Interpolation, ParserError, TemplateBinding, VariableBinding} from '@angular/compiler/src/expression_parser/ast';
import {AbsoluteSourceSpan, ASTWithSource, BindingPipe, EmptyExpr, Interpolation, MethodCall, ParserError, TemplateBinding, VariableBinding} from '@angular/compiler/src/expression_parser/ast';
import {Lexer} from '@angular/compiler/src/expression_parser/lexer';
import {IvyParser, Parser, SplitInterpolation} from '@angular/compiler/src/expression_parser/parser';
import {expect} from '@angular/platform-browser/testing/src/matchers';
@ -350,6 +350,11 @@ describe('parser', () => {
expect(unparseWithSpan(ast)).toContain(['foo()', '[nameSpan] foo']);
});
it('should record method call argument span', () => {
const ast = parseAction('foo(1 + 2)');
expect(unparseWithSpan(ast)).toContain(['foo(1 + 2)', '[argumentSpan] 1 + 2']);
});
it('should record accessed method call span', () => {
const ast = parseAction('foo.bar()');
expect(unparseWithSpan(ast)).toContain(['foo.bar()', 'foo.bar()']);

View File

@ -229,6 +229,9 @@ export function unparseWithSpan(
if (ast.hasOwnProperty('nameSpan')) {
this.recordUnparsed(ast, 'nameSpan', unparsedList);
}
if (ast.hasOwnProperty('argumentSpan')) {
this.recordUnparsed(ast, 'argumentSpan', unparsedList);
}
ast.visit(this, unparsedList);
}
};