feat(compiler): add name spans for property reads and method calls (#36826)
ASTs for property read and method calls contain information about the entire span of the expression, including its receiver. Use cases like a language service and compile error messages may be more interested in the span of the direct identifier for which the expression is constructed (i.e. an accessed property). To support this, this commit adds a `nameSpan` property on - `PropertyRead`s - `SafePropertyRead`s - `PropertyWrite`s - `MethodCall`s - `SafeMethodCall`s The `nameSpan` property already existed for `BindingPipe`s. This commit also updates usages of these expressions' `sourceSpan`s in Ngtsc and the langauge service to use `nameSpan`s where appropriate. PR Close #36826
This commit is contained in:
@ -12,7 +12,7 @@ import {Parser, SplitInterpolation} from '@angular/compiler/src/expression_parse
|
||||
import {expect} from '@angular/platform-browser/testing/src/matchers';
|
||||
|
||||
|
||||
import {unparse} from './utils/unparser';
|
||||
import {unparse, unparseWithSpan} from './utils/unparser';
|
||||
import {validate} from './utils/validator';
|
||||
|
||||
describe('parser', () => {
|
||||
@ -198,6 +198,56 @@ describe('parser', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('parse spans', () => {
|
||||
it('should record property read span', () => {
|
||||
const ast = parseAction('foo');
|
||||
expect(unparseWithSpan(ast)).toContain(['foo', 'foo']);
|
||||
expect(unparseWithSpan(ast)).toContain(['foo', '[nameSpan] foo']);
|
||||
});
|
||||
|
||||
it('should record accessed property read span', () => {
|
||||
const ast = parseAction('foo.bar');
|
||||
expect(unparseWithSpan(ast)).toContain(['foo.bar', 'foo.bar']);
|
||||
expect(unparseWithSpan(ast)).toContain(['foo.bar', '[nameSpan] bar']);
|
||||
});
|
||||
|
||||
it('should record safe property read span', () => {
|
||||
const ast = parseAction('foo?.bar');
|
||||
expect(unparseWithSpan(ast)).toContain(['foo?.bar', 'foo?.bar']);
|
||||
expect(unparseWithSpan(ast)).toContain(['foo?.bar', '[nameSpan] bar']);
|
||||
});
|
||||
|
||||
it('should record method call span', () => {
|
||||
const ast = parseAction('foo()');
|
||||
expect(unparseWithSpan(ast)).toContain(['foo()', 'foo()']);
|
||||
expect(unparseWithSpan(ast)).toContain(['foo()', '[nameSpan] foo']);
|
||||
});
|
||||
|
||||
it('should record accessed method call span', () => {
|
||||
const ast = parseAction('foo.bar()');
|
||||
expect(unparseWithSpan(ast)).toContain(['foo.bar()', 'foo.bar()']);
|
||||
expect(unparseWithSpan(ast)).toContain(['foo.bar()', '[nameSpan] bar']);
|
||||
});
|
||||
|
||||
it('should record safe method call span', () => {
|
||||
const ast = parseAction('foo?.bar()');
|
||||
expect(unparseWithSpan(ast)).toContain(['foo?.bar()', 'foo?.bar()']);
|
||||
expect(unparseWithSpan(ast)).toContain(['foo?.bar()', '[nameSpan] bar']);
|
||||
});
|
||||
|
||||
it('should record property write span', () => {
|
||||
const ast = parseAction('a = b');
|
||||
expect(unparseWithSpan(ast)).toContain(['a = b', 'a = b']);
|
||||
expect(unparseWithSpan(ast)).toContain(['a = b', '[nameSpan] a']);
|
||||
});
|
||||
|
||||
it('should record accessed property write span', () => {
|
||||
const ast = parseAction('a.b = c');
|
||||
expect(unparseWithSpan(ast)).toContain(['a.b = c', 'a.b = c']);
|
||||
expect(unparseWithSpan(ast)).toContain(['a.b = c', '[nameSpan] b']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('general error handling', () => {
|
||||
it('should report an unexpected token', () => {
|
||||
expectActionError('[1,2] trac', 'Unexpected token \'trac\'');
|
||||
@ -589,7 +639,7 @@ describe('parser', () => {
|
||||
['of: [1,2,3] | pipe ', 'of', '[1,2,3] | pipe'],
|
||||
['of: [1,2,3] | pipe as items; ', 'items', 'of'],
|
||||
['let i=index, ', 'i', 'index'],
|
||||
['count as len, ', 'len', 'count'],
|
||||
['count as len,', 'len', 'count'],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
@ -6,7 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {AST, AstVisitor, Binary, BindingPipe, Chain, Conditional, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, NonNullAssert, PrefixNot, PropertyRead, PropertyWrite, Quote, SafeMethodCall, SafePropertyRead} from '../../../src/expression_parser/ast';
|
||||
import {AbsoluteSourceSpan, AST, AstVisitor, ASTWithSource, Binary, BindingPipe, Chain, Conditional, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, NonNullAssert, ParseSpan, PrefixNot, PropertyRead, PropertyWrite, Quote, RecursiveAstVisitor, SafeMethodCall, SafePropertyRead} from '../../../src/expression_parser/ast';
|
||||
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../../../src/ml_parser/interpolation_config';
|
||||
|
||||
class Unparser implements AstVisitor {
|
||||
@ -197,3 +197,34 @@ export function unparse(
|
||||
ast: AST, interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): string {
|
||||
return sharedUnparser.unparse(ast, interpolationConfig);
|
||||
}
|
||||
|
||||
// [unparsed AST, original source code of AST]
|
||||
type UnparsedWithSpan = [string, string];
|
||||
|
||||
export function unparseWithSpan(
|
||||
ast: ASTWithSource,
|
||||
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): UnparsedWithSpan[] {
|
||||
const unparsed: UnparsedWithSpan[] = [];
|
||||
const source = ast.source!;
|
||||
const recursiveSpanUnparser = new class extends RecursiveAstVisitor {
|
||||
private recordUnparsed(ast: any, spanKey: string, unparsedList: UnparsedWithSpan[]) {
|
||||
const span = ast[spanKey];
|
||||
const prefix = spanKey === 'span' ? '' : `[${spanKey}] `;
|
||||
const src = source.substring(span.start, span.end);
|
||||
unparsedList.push([
|
||||
unparse(ast, interpolationConfig),
|
||||
prefix + src,
|
||||
]);
|
||||
}
|
||||
|
||||
visit(ast: AST, unparsedList: UnparsedWithSpan[]) {
|
||||
this.recordUnparsed(ast, 'span', unparsedList);
|
||||
if (ast.hasOwnProperty('nameSpan')) {
|
||||
this.recordUnparsed(ast, 'nameSpan', unparsedList);
|
||||
}
|
||||
ast.visit(this, unparsedList);
|
||||
}
|
||||
};
|
||||
recursiveSpanUnparser.visitAll([ast.ast], unparsed);
|
||||
return unparsed;
|
||||
}
|
||||
|
Reference in New Issue
Block a user