/** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ import {absoluteFrom, absoluteFrom as _} from '@angular/compiler-cli/src/ngtsc/file_system'; import {initMockFileSystem, TestFile} from '@angular/compiler-cli/src/ngtsc/file_system/testing'; import * as ts from 'typescript/lib/tsserverlibrary'; import {extractCursorInfo, LanguageServiceTestEnvironment} from './env'; import {createModuleWithDeclarations, getText} from './test_utils'; describe('find references', () => { let env: LanguageServiceTestEnvironment; beforeEach(() => { initMockFileSystem('Native'); }); it('gets component member references from TS file', () => { const {text, cursor} = extractCursorInfo(` import {Component} from '@angular/core'; @Component({templateUrl: './app.html'}) export class AppCmp { myP¦rop!: string; }`); const appFile = {name: _('/app.ts'), contents: text}; const templateFile = {name: _('/app.html'), contents: '{{myProp}}'}; env = createModuleWithDeclarations([appFile], [templateFile]); const refs = getReferencesAtPosition(_('/app.ts'), cursor)!; expect(refs.length).toBe(2); assertFileNames(refs, ['app.html', 'app.ts']); assertTextSpans(refs, ['myProp']); }); it('gets component member references from TS file and inline template', () => { const {text, cursor} = extractCursorInfo(` import {Component} from '@angular/core'; @Component({template: '{{myProp}}'}) export class AppCmp { myP¦rop!: string; }`); const appFile = {name: _('/app.ts'), contents: text}; env = createModuleWithDeclarations([appFile]); const refs = getReferencesAtPosition(_('/app.ts'), cursor)!; expect(refs.length).toBe(2); assertFileNames(refs, ['app.ts']); assertTextSpans(refs, ['myProp']); }); it('gets component member references from template', () => { const appFile = { name: _('/app.ts'), contents: ` import {Component} from '@angular/core'; @Component({templateUrl: './app.html'}) export class AppCmp { myProp = ''; }`, }; const {text, cursor} = extractCursorInfo('{{myP¦rop}}'); const templateFile = {name: _('/app.html'), contents: text}; env = createModuleWithDeclarations([appFile], [templateFile]); const refs = getReferencesAtPosition(_('/app.html'), cursor)!; expect(refs.length).toBe(2); assertFileNames(refs, ['app.html', 'app.ts']); assertTextSpans(refs, ['myProp']); }); it('should work for method calls', () => { const {text, cursor} = extractCursorInfo(` import {Component} from '@angular/core'; @Component({template: '
'}) export class AppCmp { setTitle(s: number) {} }`); const appFile = {name: _('/app.ts'), contents: text}; env = createModuleWithDeclarations([appFile]); const refs = getReferencesAtPosition(_('/app.ts'), cursor)!; expect(refs.length).toBe(2); assertFileNames(refs, ['app.ts']); assertTextSpans(refs, ['setTitle']); }); it('should work for method call arguments', () => { const {text, cursor} = extractCursorInfo(` import {Component} from '@angular/core'; @Component({template: ''}) export class AppCmp { title = ''; setTitle(s: string) {} }`); const appFile = {name: _('/app.ts'), contents: text}; env = createModuleWithDeclarations([appFile]); const refs = getReferencesAtPosition(_('/app.ts'), cursor)!; expect(refs.length).toBe(2); assertTextSpans(refs, ['title']); }); it('should work for property writes', () => { const appFile = { name: _('/app.ts'), contents: ` import {Component} from '@angular/core'; @Component({templateUrl: './app.html' }) export class AppCmp { title = ''; }`, }; const templateFileWithCursor = ``; const {text, cursor} = extractCursorInfo(templateFileWithCursor); const templateFile = {name: _('/app.html'), contents: text}; env = createModuleWithDeclarations([appFile], [templateFile]); const refs = getReferencesAtPosition(_('/app.html'), cursor)!; expect(refs.length).toBe(2); assertFileNames(refs, ['app.ts', 'app.html']); assertTextSpans(refs, ['title']); }); it('should work for RHS of property writes', () => { const {text, cursor} = extractCursorInfo(` import {Component} from '@angular/core'; @Component({template: '' }) export class AppCmp { title = ''; otherTitle = ''; }`); const appFile = { name: _('/app.ts'), contents: text, }; env = createModuleWithDeclarations([appFile]); const refs = getReferencesAtPosition(_('/app.ts'), cursor)!; expect(refs.length).toBe(2); assertFileNames(refs, ['app.ts']); assertTextSpans(refs, ['otherTitle']); }); it('should work for keyed reads', () => { const {text, cursor} = extractCursorInfo(` import {Component} from '@angular/core'; @Component({template: '{{hero["na¦me"]}}' }) export class AppCmp { hero: {name: string} = {name: 'Superman'}; }`); const appFile = { name: _('/app.ts'), contents: text, }; env = createModuleWithDeclarations([appFile]); const refs = getReferencesAtPosition(_('/app.ts'), cursor)!; // 3 references: the type definition, the value assignment, and the read in the template expect(refs.length).toBe(3); assertFileNames(refs, ['app.ts']); // TODO(atscott): investigate if we can make the template keyed read be just the 'name' part. // The TypeScript implementation specifically adjusts the span to accommodate string literals: // https://sourcegraph.com/github.com/microsoft/TypeScript@d5779c75d3dd19565b60b9e2960b8aac36d4d635/-/blob/src/services/findAllReferences.ts#L508-512 // One possible solution would be to extend `FullTemplateMapping` to include the matched TCB // node and then do the same thing that TS does: if the node is a string, adjust the span. assertTextSpans(refs, ['name', '"name"']); }); it('should work for keyed writes', () => { const appFile = { name: _('/app.ts'), contents: ` import {Component} from '@angular/core'; @Component({templateUrl: './app.html' }) export class AppCmp { hero: {name: string} = {name: 'Superman'}; batman = 'batman'; }`, }; const templateFileWithCursor = ``; const {text, cursor} = extractCursorInfo(templateFileWithCursor); const templateFile = {name: _('/app.html'), contents: text}; env = createModuleWithDeclarations([appFile], [templateFile]); const refs = getReferencesAtPosition(_('/app.html'), cursor)!; expect(refs.length).toBe(2); assertFileNames(refs, ['app.ts', 'app.html']); assertTextSpans(refs, ['batman']); }); describe('references', () => { it('should work for element references', () => { const {text, cursor} = extractCursorInfo(` import {Component} from '@angular/core'; @Component({template: ' {{ myIn¦put.value }}'}) export class AppCmp { title = ''; }`); const appFile = {name: _('/app.ts'), contents: text}; env = createModuleWithDeclarations([appFile]); const refs = getReferencesAtPosition(_('/app.ts'), cursor)!; expect(refs.length).toBe(2); assertTextSpans(refs, ['myInput']); const originalRefs = env.ngLS.getReferencesAtPosition(_('/app.ts'), cursor)!; // Get the declaration by finding the reference that appears first in the template originalRefs.sort((a, b) => a.textSpan.start - b.textSpan.start); expect(originalRefs[0].isDefinition).toBe(true); }); it('should work for template references', () => { const templateWithCursor = `