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:
@ -457,8 +457,13 @@ class ExpressionVisitor extends NullTemplateVisitor {
|
||||
const absValueOffset = ast.sourceSpan.start.offset;
|
||||
const {templateBindings} = this.info.expressionParser.parseTemplateBindings(
|
||||
templateKey, templateValue, templateUrl, absKeyOffset, absValueOffset);
|
||||
// Find the template binding that contains the position.
|
||||
const templateBinding = templateBindings.find(b => inSpan(this.position, b.sourceSpan));
|
||||
// Find the nearest template binding to the position.
|
||||
const lastBindingEnd = templateBindings.length > 0 &&
|
||||
templateBindings[templateBindings.length - 1].sourceSpan.end;
|
||||
const normalizedPositionToBinding =
|
||||
lastBindingEnd && this.position > lastBindingEnd ? lastBindingEnd : this.position;
|
||||
const templateBinding =
|
||||
templateBindings.find(b => inSpan(normalizedPositionToBinding, b.sourceSpan));
|
||||
|
||||
if (!templateBinding) {
|
||||
return;
|
||||
|
@ -6,11 +6,12 @@
|
||||
* 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 '@angular/compiler';
|
||||
import {AST, AstVisitor, ASTWithName, Binary, BindingPipe, Chain, Conditional, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, NonNullAssert, PrefixNot, PropertyRead, PropertyWrite, Quote, SafeMethodCall, SafePropertyRead} from '@angular/compiler';
|
||||
|
||||
import {createDiagnostic, Diagnostic} from './diagnostic_messages';
|
||||
import {BuiltinType, Signature, Symbol, SymbolQuery, SymbolTable} from './symbols';
|
||||
import * as ng from './types';
|
||||
import {offsetSpan} from './utils';
|
||||
|
||||
interface ExpressionDiagnosticsContext {
|
||||
inEvent?: boolean;
|
||||
@ -32,7 +33,7 @@ export class AstType implements AstVisitor {
|
||||
const type: Symbol = ast.visit(this);
|
||||
if (this.context.inEvent && type.callable) {
|
||||
this.diagnostics.push(
|
||||
createDiagnostic(ast.span, Diagnostic.callable_expression_expected_method_call));
|
||||
createDiagnostic(refinedSpan(ast), Diagnostic.callable_expression_expected_method_call));
|
||||
}
|
||||
return this.diagnostics;
|
||||
}
|
||||
@ -51,7 +52,8 @@ export class AstType implements AstVisitor {
|
||||
// Nullable allowed.
|
||||
break;
|
||||
default:
|
||||
this.diagnostics.push(createDiagnostic(ast.span, Diagnostic.expression_might_be_null));
|
||||
this.diagnostics.push(
|
||||
createDiagnostic(refinedSpan(ast), Diagnostic.expression_might_be_null));
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -130,7 +132,7 @@ export class AstType implements AstVisitor {
|
||||
return this.anyType;
|
||||
default:
|
||||
this.diagnostics.push(
|
||||
createDiagnostic(ast.span, Diagnostic.expected_a_string_or_number_type));
|
||||
createDiagnostic(refinedSpan(ast), Diagnostic.expected_a_string_or_number_type));
|
||||
return this.anyType;
|
||||
}
|
||||
case '>':
|
||||
@ -146,8 +148,8 @@ export class AstType implements AstVisitor {
|
||||
// Two values are comparable only if
|
||||
// - they have some type overlap, or
|
||||
// - at least one is not defined
|
||||
this.diagnostics.push(
|
||||
createDiagnostic(ast.span, Diagnostic.expected_operands_of_comparable_types_or_any));
|
||||
this.diagnostics.push(createDiagnostic(
|
||||
refinedSpan(ast), Diagnostic.expected_operands_of_comparable_types_or_any));
|
||||
}
|
||||
return this.query.getBuiltinType(BuiltinType.Boolean);
|
||||
case '&&':
|
||||
@ -157,7 +159,7 @@ export class AstType implements AstVisitor {
|
||||
}
|
||||
|
||||
this.diagnostics.push(
|
||||
createDiagnostic(ast.span, Diagnostic.unrecognized_operator, ast.operation));
|
||||
createDiagnostic(refinedSpan(ast), Diagnostic.unrecognized_operator, ast.operation));
|
||||
return this.anyType;
|
||||
}
|
||||
|
||||
@ -187,7 +189,8 @@ export class AstType implements AstVisitor {
|
||||
const target = this.getType(ast.target!);
|
||||
if (!target || !target.callable) {
|
||||
this.diagnostics.push(createDiagnostic(
|
||||
ast.span, Diagnostic.call_target_not_callable, this.sourceOf(ast.target!), target.name));
|
||||
refinedSpan(ast), Diagnostic.call_target_not_callable, this.sourceOf(ast.target!),
|
||||
target.name));
|
||||
return this.anyType;
|
||||
}
|
||||
const signature = target.selectSignature(args);
|
||||
@ -197,7 +200,7 @@ export class AstType implements AstVisitor {
|
||||
// TODO: Consider a better error message here. See `typescript_symbols#selectSignature` for more
|
||||
// details.
|
||||
this.diagnostics.push(
|
||||
createDiagnostic(ast.span, Diagnostic.unable_to_resolve_compatible_call_signature));
|
||||
createDiagnostic(refinedSpan(ast), Diagnostic.unable_to_resolve_compatible_call_signature));
|
||||
return this.anyType;
|
||||
}
|
||||
|
||||
@ -291,8 +294,8 @@ export class AstType implements AstVisitor {
|
||||
case 'number':
|
||||
return this.query.getBuiltinType(BuiltinType.Number);
|
||||
default:
|
||||
this.diagnostics.push(
|
||||
createDiagnostic(ast.span, Diagnostic.unrecognized_primitive, typeof ast.value));
|
||||
this.diagnostics.push(createDiagnostic(
|
||||
refinedSpan(ast), Diagnostic.unrecognized_primitive, typeof ast.value));
|
||||
return this.anyType;
|
||||
}
|
||||
}
|
||||
@ -307,7 +310,7 @@ export class AstType implements AstVisitor {
|
||||
// by getPipes() is expected to contain symbols with the corresponding transform method type.
|
||||
const pipe = this.query.getPipes().get(ast.name);
|
||||
if (!pipe) {
|
||||
this.diagnostics.push(createDiagnostic(ast.span, Diagnostic.no_pipe_found, ast.name));
|
||||
this.diagnostics.push(createDiagnostic(refinedSpan(ast), Diagnostic.no_pipe_found, ast.name));
|
||||
return this.anyType;
|
||||
}
|
||||
const expType = this.getType(ast.exp);
|
||||
@ -315,7 +318,7 @@ export class AstType implements AstVisitor {
|
||||
pipe.selectSignature([expType].concat(ast.args.map(arg => this.getType(arg))));
|
||||
if (!signature) {
|
||||
this.diagnostics.push(
|
||||
createDiagnostic(ast.span, Diagnostic.unable_to_resolve_signature, ast.name));
|
||||
createDiagnostic(refinedSpan(ast), Diagnostic.unable_to_resolve_signature, ast.name));
|
||||
return this.anyType;
|
||||
}
|
||||
return signature.result;
|
||||
@ -389,7 +392,7 @@ export class AstType implements AstVisitor {
|
||||
const methodType = this.resolvePropertyRead(receiverType, ast);
|
||||
if (!methodType) {
|
||||
this.diagnostics.push(
|
||||
createDiagnostic(ast.span, Diagnostic.could_not_resolve_type, ast.name));
|
||||
createDiagnostic(refinedSpan(ast), Diagnostic.could_not_resolve_type, ast.name));
|
||||
return this.anyType;
|
||||
}
|
||||
if (this.isAny(methodType)) {
|
||||
@ -397,13 +400,13 @@ export class AstType implements AstVisitor {
|
||||
}
|
||||
if (!methodType.callable) {
|
||||
this.diagnostics.push(
|
||||
createDiagnostic(ast.span, Diagnostic.identifier_not_callable, ast.name));
|
||||
createDiagnostic(refinedSpan(ast), Diagnostic.identifier_not_callable, ast.name));
|
||||
return this.anyType;
|
||||
}
|
||||
const signature = methodType.selectSignature(ast.args.map(arg => this.getType(arg)));
|
||||
if (!signature) {
|
||||
this.diagnostics.push(
|
||||
createDiagnostic(ast.span, Diagnostic.unable_to_resolve_signature, ast.name));
|
||||
createDiagnostic(refinedSpan(ast), Diagnostic.unable_to_resolve_signature, ast.name));
|
||||
return this.anyType;
|
||||
}
|
||||
return signature.result;
|
||||
@ -417,24 +420,25 @@ export class AstType implements AstVisitor {
|
||||
const member = receiverType.members().get(ast.name);
|
||||
if (!member) {
|
||||
if (receiverType.name === '$implicit') {
|
||||
this.diagnostics.push(
|
||||
createDiagnostic(ast.span, Diagnostic.identifier_not_defined_in_app_context, ast.name));
|
||||
this.diagnostics.push(createDiagnostic(
|
||||
refinedSpan(ast), Diagnostic.identifier_not_defined_in_app_context, ast.name));
|
||||
} else if (receiverType.nullable && ast.receiver instanceof PropertyRead) {
|
||||
const receiver = ast.receiver.name;
|
||||
this.diagnostics.push(createDiagnostic(
|
||||
ast.span, Diagnostic.identifier_possibly_undefined, receiver,
|
||||
refinedSpan(ast), Diagnostic.identifier_possibly_undefined, receiver,
|
||||
`${receiver}?.${ast.name}`, `${receiver}!.${ast.name}`));
|
||||
} else {
|
||||
this.diagnostics.push(createDiagnostic(
|
||||
ast.span, Diagnostic.identifier_not_defined_on_receiver, ast.name, receiverType.name));
|
||||
refinedSpan(ast), Diagnostic.identifier_not_defined_on_receiver, ast.name,
|
||||
receiverType.name));
|
||||
}
|
||||
return this.anyType;
|
||||
}
|
||||
if (!member.public) {
|
||||
const container =
|
||||
receiverType.name === '$implicit' ? 'the component' : `'${receiverType.name}'`;
|
||||
this.diagnostics.push(
|
||||
createDiagnostic(ast.span, Diagnostic.identifier_is_private, ast.name, container));
|
||||
this.diagnostics.push(createDiagnostic(
|
||||
refinedSpan(ast), Diagnostic.identifier_is_private, ast.name, container));
|
||||
}
|
||||
return member.type;
|
||||
}
|
||||
@ -444,3 +448,14 @@ export class AstType implements AstVisitor {
|
||||
(!!symbol.type && this.isAny(symbol.type));
|
||||
}
|
||||
}
|
||||
|
||||
function refinedSpan(ast: AST): ng.Span {
|
||||
// nameSpan is an absolute span, but the spans returned by the expression visitor are expected to
|
||||
// be relative to the start of the expression.
|
||||
// TODO: migrate to only using absolute spans
|
||||
const absoluteOffset = ast.sourceSpan.start - ast.span.start;
|
||||
if (ast instanceof ASTWithName) {
|
||||
return offsetSpan(ast.nameSpan, -absoluteOffset);
|
||||
}
|
||||
return offsetSpan(ast.sourceSpan, -absoluteOffset);
|
||||
}
|
||||
|
@ -6,7 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {AST, AstPath as AstPathBase, ASTWithSource, RecursiveAstVisitor} from '@angular/compiler';
|
||||
import {AST, AstPath as AstPathBase, ASTWithName, ASTWithSource, RecursiveAstVisitor} from '@angular/compiler';
|
||||
|
||||
import {AstType} from './expression_type';
|
||||
import {BuiltinType, Span, Symbol, SymbolTable, TemplateSource} from './types';
|
||||
@ -120,6 +120,17 @@ export function getExpressionSymbol(
|
||||
return new AstType(scope, templateInfo.query, {}, templateInfo.source).getType(ast);
|
||||
}
|
||||
|
||||
function spanFromName(ast: ASTWithName): Span {
|
||||
// `nameSpan` is an absolute span, but the span expected by the result of this method is
|
||||
// relative to the start of the expression.
|
||||
// TODO(ayazhafiz): migrate to only using absolute spans
|
||||
const offset = ast.sourceSpan.start - ast.span.start;
|
||||
return {
|
||||
start: ast.nameSpan.start - offset,
|
||||
end: ast.nameSpan.end - offset,
|
||||
};
|
||||
}
|
||||
|
||||
let symbol: Symbol|undefined = undefined;
|
||||
let span: Span|undefined = undefined;
|
||||
|
||||
@ -141,22 +152,14 @@ export function getExpressionSymbol(
|
||||
visitMethodCall(ast) {
|
||||
const receiverType = getType(ast.receiver);
|
||||
symbol = receiverType && receiverType.members().get(ast.name);
|
||||
span = ast.span;
|
||||
span = spanFromName(ast);
|
||||
},
|
||||
visitPipe(ast) {
|
||||
if (inSpan(position, ast.nameSpan, /* exclusive */ true)) {
|
||||
// We are in a position a pipe name is expected.
|
||||
const pipes = templateInfo.query.getPipes();
|
||||
symbol = pipes.get(ast.name);
|
||||
|
||||
// `nameSpan` is an absolute span, but the span expected by the result of this method is
|
||||
// relative to the start of the expression.
|
||||
// TODO(ayazhafiz): migrate to only using absolute spans
|
||||
const offset = ast.sourceSpan.start - ast.span.start;
|
||||
span = {
|
||||
start: ast.nameSpan.start - offset,
|
||||
end: ast.nameSpan.end - offset,
|
||||
};
|
||||
span = spanFromName(ast);
|
||||
}
|
||||
},
|
||||
visitPrefixNot(_ast) {},
|
||||
@ -164,29 +167,23 @@ export function getExpressionSymbol(
|
||||
visitPropertyRead(ast) {
|
||||
const receiverType = getType(ast.receiver);
|
||||
symbol = receiverType && receiverType.members().get(ast.name);
|
||||
span = ast.span;
|
||||
span = spanFromName(ast);
|
||||
},
|
||||
visitPropertyWrite(ast) {
|
||||
const receiverType = getType(ast.receiver);
|
||||
const {start} = ast.span;
|
||||
symbol = receiverType && receiverType.members().get(ast.name);
|
||||
// A PropertyWrite span includes both the LHS (name) and the RHS (value) of the write. In this
|
||||
// visit, only the name is relevant.
|
||||
// prop=$event
|
||||
// ^^^^ name
|
||||
// ^^^^^^ value; visited separately as a nested AST
|
||||
span = {start, end: start + ast.name.length};
|
||||
span = spanFromName(ast);
|
||||
},
|
||||
visitQuote(_ast) {},
|
||||
visitSafeMethodCall(ast) {
|
||||
const receiverType = getType(ast.receiver);
|
||||
symbol = receiverType && receiverType.members().get(ast.name);
|
||||
span = ast.span;
|
||||
span = spanFromName(ast);
|
||||
},
|
||||
visitSafePropertyRead(ast) {
|
||||
const receiverType = getType(ast.receiver);
|
||||
symbol = receiverType && receiverType.members().get(ast.name);
|
||||
span = ast.span;
|
||||
span = spanFromName(ast);
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -77,7 +77,7 @@ describe('definitions', () => {
|
||||
it('should be able to find a method from a call', () => {
|
||||
const fileName = mockHost.addCode(`
|
||||
@Component({
|
||||
template: '<div (click)="~{start-my}«myClick»()~{end-my};"></div>'
|
||||
template: '<div (click)="«myClick»();"></div>'
|
||||
})
|
||||
export class MyComponent {
|
||||
«ᐱmyClickᐱ() { }»
|
||||
@ -88,7 +88,7 @@ describe('definitions', () => {
|
||||
expect(result).toBeDefined();
|
||||
const {textSpan, definitions} = result!;
|
||||
|
||||
expect(textSpan).toEqual(mockHost.getLocationMarkerFor(fileName, 'my'));
|
||||
expect(textSpan).toEqual(marker);
|
||||
expect(definitions).toBeDefined();
|
||||
expect(definitions!.length).toBe(1);
|
||||
const def = definitions![0];
|
||||
|
@ -182,7 +182,7 @@ describe('diagnostics', () => {
|
||||
mockHost.override(TEST_TEMPLATE, `
|
||||
<div *ngIf="title; let titleProxy;">
|
||||
'titleProxy' is a string
|
||||
{{~{start-err}titleProxy.notAProperty~{end-err}}}
|
||||
{{titleProxy.~{start-err}notAProperty~{end-err}}}
|
||||
</div>
|
||||
`);
|
||||
const diags = ngLS.getSemanticDiagnostics(TEST_TEMPLATE);
|
||||
@ -200,7 +200,7 @@ describe('diagnostics', () => {
|
||||
mockHost.override(TEST_TEMPLATE, `
|
||||
<div *ngIf="title as titleProxy">
|
||||
'titleProxy' is a string
|
||||
{{~{start-err}titleProxy.notAProperty~{end-err}}}
|
||||
{{titleProxy.~{start-err}notAProperty~{end-err}}}
|
||||
</div>
|
||||
`);
|
||||
const diags = ngLS.getSemanticDiagnostics(TEST_TEMPLATE);
|
||||
@ -364,7 +364,7 @@ describe('diagnostics', () => {
|
||||
it('report an unknown field in $implicit context', () => {
|
||||
mockHost.override(TEST_TEMPLATE, `
|
||||
<div *withContext="let myVar">
|
||||
{{ ~{start-emb}myVar.missingField~{end-emb} }}
|
||||
{{ myVar.~{start-emb}missingField~{end-emb} }}
|
||||
</div>
|
||||
`);
|
||||
const diags = ngLS.getSemanticDiagnostics(TEST_TEMPLATE);
|
||||
@ -383,7 +383,7 @@ describe('diagnostics', () => {
|
||||
it('report an unknown field in non implicit context', () => {
|
||||
mockHost.override(TEST_TEMPLATE, `
|
||||
<div *withContext="let myVar = nonImplicitPerson">
|
||||
{{ ~{start-emb}myVar.missingField~{end-emb} }}
|
||||
{{ myVar.~{start-emb}missingField~{end-emb} }}
|
||||
</div>
|
||||
`);
|
||||
const diags = ngLS.getSemanticDiagnostics(TEST_TEMPLATE);
|
||||
@ -421,8 +421,7 @@ describe('diagnostics', () => {
|
||||
const {messageText, start, length} = diagnostics[0];
|
||||
expect(messageText)
|
||||
.toBe(`Identifier 'xyz' is not defined. 'Hero' does not contain such a member`);
|
||||
expect(start).toBe(content.indexOf('member.xyz'));
|
||||
expect(length).toBe('member.xyz'.length);
|
||||
expect(content.substring(start!, start! + length!)).toBe('xyz');
|
||||
});
|
||||
|
||||
describe('with $event', () => {
|
||||
@ -454,8 +453,7 @@ describe('diagnostics', () => {
|
||||
expect(messageText)
|
||||
.toBe(
|
||||
`Identifier 'notSubstring' is not defined. 'string' does not contain such a member`);
|
||||
expect(start).toBe(content.indexOf('$event'));
|
||||
expect(length).toBe('$event.notSubstring()'.length);
|
||||
expect(content.substring(start!, start! + length!)).toBe('notSubstring');
|
||||
});
|
||||
});
|
||||
|
||||
@ -990,7 +988,7 @@ describe('diagnostics', () => {
|
||||
`Consider using the safe navigation operator (optional?.toLowerCase) ` +
|
||||
`or non-null assertion operator (optional!.toLowerCase).`);
|
||||
expect(category).toBe(ts.DiagnosticCategory.Suggestion);
|
||||
expect(content.substring(start!, start! + length!)).toBe('optional.toLowerCase()');
|
||||
expect(content.substring(start!, start! + length!)).toBe('toLowerCase');
|
||||
});
|
||||
|
||||
it('should suggest ? or ! operator if property receiver is nullable', () => {
|
||||
@ -1004,40 +1002,40 @@ describe('diagnostics', () => {
|
||||
`Consider using the safe navigation operator (optional?.length) ` +
|
||||
`or non-null assertion operator (optional!.length).`);
|
||||
expect(category).toBe(ts.DiagnosticCategory.Suggestion);
|
||||
expect(content.substring(start!, start! + length!)).toBe('optional.length');
|
||||
expect(content.substring(start!, start! + length!)).toBe('length');
|
||||
});
|
||||
|
||||
it('should report error if method is not found on non-nullable receiver', () => {
|
||||
it('should report error if method is not found on non-nullable receivers', () => {
|
||||
const expressions = [
|
||||
'optional?.someMethod()',
|
||||
'optional!.someMethod()',
|
||||
'optional?',
|
||||
'optional!',
|
||||
];
|
||||
for (const expression of expressions) {
|
||||
const content = mockHost.override(TEST_TEMPLATE, `{{${expression}}}`);
|
||||
const content = mockHost.override(TEST_TEMPLATE, `{{ ${expression}.someMethod() }}`);
|
||||
const ngDiags = ngLS.getSemanticDiagnostics(TEST_TEMPLATE);
|
||||
expect(ngDiags.length).toBe(1);
|
||||
const {start, length, messageText, category} = ngDiags[0];
|
||||
expect(messageText)
|
||||
.toBe(`Identifier 'someMethod' is not defined. 'string' does not contain such a member`);
|
||||
expect(category).toBe(ts.DiagnosticCategory.Error);
|
||||
expect(content.substring(start!, start! + length!)).toBe(expression);
|
||||
expect(content.substring(start!, start! + length!)).toBe('someMethod');
|
||||
}
|
||||
});
|
||||
|
||||
it('should report error if property is not found on non-nullable receiver', () => {
|
||||
it('should report error if property is not found on non-nullable receivers', () => {
|
||||
const expressions = [
|
||||
'optional?.someProp',
|
||||
'optional!.someProp',
|
||||
'optional?',
|
||||
'optional!',
|
||||
];
|
||||
for (const expression of expressions) {
|
||||
const content = mockHost.override(TEST_TEMPLATE, `{{${expression}}}`);
|
||||
const content = mockHost.override(TEST_TEMPLATE, `{{ ${expression}.someProp }}`);
|
||||
const ngDiags = ngLS.getSemanticDiagnostics(TEST_TEMPLATE);
|
||||
expect(ngDiags.length).toBe(1);
|
||||
const {start, length, messageText, category} = ngDiags[0];
|
||||
expect(messageText)
|
||||
.toBe(`Identifier 'someProp' is not defined. 'string' does not contain such a member`);
|
||||
expect(category).toBe(ts.DiagnosticCategory.Error);
|
||||
expect(content.substring(start!, start! + length!)).toBe(expression);
|
||||
expect(content.substring(start!, start! + length!)).toBe('someProp');
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -109,6 +109,36 @@ describe('hover', () => {
|
||||
expect(toText(displayParts)).toBe('(property) TemplateReference.title: string');
|
||||
});
|
||||
|
||||
it('should work for accessed property reads', () => {
|
||||
mockHost.override(TEST_TEMPLATE, `<div>{{title.«length»}}</div>`);
|
||||
const marker = mockHost.getReferenceMarkerFor(TEST_TEMPLATE, 'length');
|
||||
const quickInfo = ngLS.getQuickInfoAtPosition(TEST_TEMPLATE, marker.start);
|
||||
expect(quickInfo).toBeTruthy();
|
||||
const {textSpan, displayParts} = quickInfo!;
|
||||
expect(textSpan).toEqual(marker);
|
||||
expect(toText(displayParts)).toBe('(property) String.length: number');
|
||||
});
|
||||
|
||||
it('should work for properties in writes', () => {
|
||||
mockHost.override(TEST_TEMPLATE, `<div (click)="«title» = 't'"></div>`);
|
||||
const marker = mockHost.getReferenceMarkerFor(TEST_TEMPLATE, 'title');
|
||||
const quickInfo = ngLS.getQuickInfoAtPosition(TEST_TEMPLATE, marker.start);
|
||||
expect(quickInfo).toBeTruthy();
|
||||
const {textSpan, displayParts} = quickInfo!;
|
||||
expect(textSpan).toEqual(marker);
|
||||
expect(toText(displayParts)).toBe('(property) TemplateReference.title: string');
|
||||
});
|
||||
|
||||
it('should work for accessed properties in writes', () => {
|
||||
mockHost.override(TEST_TEMPLATE, `<div (click)="hero.«id» = 2"></div>`);
|
||||
const marker = mockHost.getReferenceMarkerFor(TEST_TEMPLATE, 'id');
|
||||
const quickInfo = ngLS.getQuickInfoAtPosition(TEST_TEMPLATE, marker.start);
|
||||
expect(quickInfo).toBeTruthy();
|
||||
const {textSpan, displayParts} = quickInfo!;
|
||||
expect(textSpan).toEqual(marker);
|
||||
expect(toText(displayParts)).toBe('(property) Hero.id: number');
|
||||
});
|
||||
|
||||
it('should work for array members', () => {
|
||||
mockHost.override(TEST_TEMPLATE, `<div *ngFor="let hero of heroes">{{«hero»}}</div>`);
|
||||
const marker = mockHost.getReferenceMarkerFor(TEST_TEMPLATE, 'hero');
|
||||
@ -142,8 +172,8 @@ describe('hover', () => {
|
||||
});
|
||||
|
||||
it('should work for method calls', () => {
|
||||
mockHost.override(TEST_TEMPLATE, `<div (click)="«ᐱmyClickᐱ($event)»"></div>`);
|
||||
const marker = mockHost.getDefinitionMarkerFor(TEST_TEMPLATE, 'myClick');
|
||||
mockHost.override(TEST_TEMPLATE, `<div (click)="«myClick»($event)"></div>`);
|
||||
const marker = mockHost.getReferenceMarkerFor(TEST_TEMPLATE, 'myClick');
|
||||
const quickInfo = ngLS.getQuickInfoAtPosition(TEST_TEMPLATE, marker.start);
|
||||
expect(quickInfo).toBeTruthy();
|
||||
const {textSpan, displayParts} = quickInfo!;
|
||||
@ -192,7 +222,7 @@ describe('hover', () => {
|
||||
const {textSpan, displayParts} = quickInfo!;
|
||||
expect(textSpan).toEqual({
|
||||
start: position,
|
||||
length: '$any(title)'.length,
|
||||
length: '$any'.length,
|
||||
});
|
||||
expect(toText(displayParts)).toBe('(method) $any: $any');
|
||||
});
|
||||
|
Reference in New Issue
Block a user