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, leftMostSafe,
new cdAst.MethodCall( new cdAst.MethodCall(
leftMostSafe.span, leftMostSafe.sourceSpan, leftMostSafe.nameSpan, leftMostSafe.span, leftMostSafe.sourceSpan, leftMostSafe.nameSpan,
leftMostSafe.receiver, leftMostSafe.name, leftMostSafe.args)); leftMostSafe.receiver, leftMostSafe.name, leftMostSafe.args,
leftMostSafe.argumentSpan));
} else { } else {
this._nodeMap.set( this._nodeMap.set(
leftMostSafe, leftMostSafe,

View File

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

View File

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

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license * 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 {Lexer} from '@angular/compiler/src/expression_parser/lexer';
import {IvyParser, Parser, SplitInterpolation} from '@angular/compiler/src/expression_parser/parser'; import {IvyParser, Parser, SplitInterpolation} from '@angular/compiler/src/expression_parser/parser';
import {expect} from '@angular/platform-browser/testing/src/matchers'; import {expect} from '@angular/platform-browser/testing/src/matchers';
@ -350,6 +350,11 @@ describe('parser', () => {
expect(unparseWithSpan(ast)).toContain(['foo()', '[nameSpan] foo']); 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', () => { it('should record accessed method call span', () => {
const ast = parseAction('foo.bar()'); const ast = parseAction('foo.bar()');
expect(unparseWithSpan(ast)).toContain(['foo.bar()', 'foo.bar()']); expect(unparseWithSpan(ast)).toContain(['foo.bar()', 'foo.bar()']);

View File

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