Path: blob/main/extensions/copilot/test/simulation/renameSuggestionsProvider.stest.ts
13389 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/4import * as assert from 'assert';5import { outdent } from 'outdent';6import type * as vscode from 'vscode';7import { guessNamingConvention, NamingConvention } from '../../src/extension/renameSuggestions/common/namingConvention';8import { RenameSuggestionsProvider } from '../../src/extension/renameSuggestions/node/renameSuggestionsProvider';9import { TestingServiceCollection } from '../../src/platform/test/node/services';10import { IRelativeFile } from '../../src/platform/test/node/simulationWorkspace';11import { deannotateSrc } from '../../src/util/common/test/annotatedSrc';12import { CancellationToken } from '../../src/util/vs/base/common/cancellation';13import { IInstantiationService } from '../../src/util/vs/platform/instantiation/common/instantiation';14import { NewSymbolNameTriggerKind, Range } from '../../src/vscodeTypes';15import { ISimulationTestRuntime, ssuite, stest } from '../base/stest';16import { setupSimulationWorkspace, teardownSimulationWorkspace } from './inlineChatSimulator';17import { INLINE_INITIAL_DOC_TAG } from './shared/sharedTypes';1819type OffsetRange = {20startIndex: number;21endIndex: number;22};2324function offsetRangeToPositionRange(offsetRange: OffsetRange, document: vscode.TextDocument): vscode.Range {25const startPos = document.positionAt(offsetRange.startIndex);26const endPos = document.positionAt(offsetRange.endIndex);27const range = new Range(startPos, endPos);28return range;29}3031ssuite({ title: 'Rename suggestions', location: 'external' }, () => {3233class AlwaysEnabledNewSymbolNamesProvider extends RenameSuggestionsProvider {34override isEnabled() {35return true;36}37}3839/**40* Asserts that each newSymbolName includes at least one of the searchStrings.41*42* @remark lower-cases symbol names for string search but not search-strings43*/44function assertIncludesLowercased(newSymbolNames: vscode.NewSymbolName[], searchStrings: string | string[]) {45searchStrings = Array.isArray(searchStrings) ? searchStrings : [searchStrings];46searchStrings = searchStrings.map(s => s.toLowerCase());47for (const symbol of newSymbolNames) {48const newSymbolNameLowercase = symbol.newSymbolName.toLowerCase();49assert.ok(50searchStrings.some(searchString => newSymbolNameLowercase.includes(searchString)),51`expected to include ${searchStrings.map(s => `'${s}'`).join(' or ')} but received '${newSymbolNameLowercase}'`52);53}54}5556function assertLength(newSymbolNames: vscode.NewSymbolName[]) {57assert.ok(newSymbolNames.length > 1,58`expected at least ${1} newSymbolNames but received ${newSymbolNames.length}\n${JSON.stringify(newSymbolNames.map(v => v.newSymbolName), null, '\t')}`);59}6061function countMatches(newSymbolNames: vscode.NewSymbolName[], searchStrings: string) {62const searchStringsLowercased = searchStrings.toLowerCase();63return newSymbolNames.filter(symbol => symbol.newSymbolName.toLowerCase().includes(searchStringsLowercased)).length;64}6566type IRenameScenarioFile = (IRelativeFile & {67isCurrent?: boolean;68});6970async function provideNewSymbolNames(testingServiceCollection: TestingServiceCollection, files: IRenameScenarioFile[]) {7172// find current file from files, deannoate it and put it at the end7374const currentFileIx = files.length === 1 ? 0 : files.findIndex(f => f.isCurrent);75if (currentFileIx < 0) { throw new Error(`No current file found from files:\n ${JSON.stringify(files, null, '\t')}`); }76let currentFile = files[currentFileIx];77files.splice(currentFileIx, 1);78const { deannotatedSrc, annotatedRange } = deannotateSrc(currentFile.fileContents);79currentFile = {80...currentFile,81fileContents: deannotatedSrc,82};83files.push(currentFile);8485// set up workspace86const workspace = setupSimulationWorkspace(testingServiceCollection, { files });87const accessor = testingServiceCollection.createTestingAccessor();88try {89const document = workspace.getDocument(currentFile.fileName).document;90const renameRange = offsetRangeToPositionRange(annotatedRange, document);9192// write initial file contents to disk to be able to view it from swb9394const testRuntime = accessor.get(ISimulationTestRuntime);95const workspacePath = workspace.getFilePath(document.uri);96await testRuntime.writeFile(workspacePath + '.txt', document.getText(), INLINE_INITIAL_DOC_TAG); // using .txt instead of real file extension to avoid breaking automation scripts9798// get rename suggestions99100const provider = accessor.get(IInstantiationService).createInstance(AlwaysEnabledNewSymbolNamesProvider);101102const symbols = await provider.provideNewSymbolNames(document, renameRange, NewSymbolNameTriggerKind.Invoke, CancellationToken.None);103104return symbols;105106} finally {107await teardownSimulationWorkspace(accessor, workspace);108}109110}111112stest('rename a function at its definition', async (testingServiceCollection) => {113const fileContents = outdent`114export function <<fibonacci>>(n: number): number {115if (n <= 1) {116return 1;117}118return fibonacci(n - 1) + fibonacci(n - 2);119}120`;121122const file: IRenameScenarioFile = {123kind: 'relativeFile',124fileName: 'fibonacci.ts',125languageId: 'typescript',126fileContents,127isCurrent: true,128};129130const symbols = await provideNewSymbolNames(testingServiceCollection, [file]);131132assert.ok(symbols, `Expected symbols to be non-null`);133assertLength(symbols);134assert.ok(countMatches(symbols, 'fib') >= Math.floor(symbols.length * 0.8), 'Expected 80% of symbols to include fib: ' + JSON.stringify(symbols.map(s => s.newSymbolName)));135});136137stest('rename follows naming convention _ - rename a function (with underscore) at its definition', async (testingServiceCollection) => {138const fileContents = outdent`139export function <<_fib>>(n: number): number {140if (n <= 1) {141return 1;142}143return _fib(n - 1) + _fib(n - 2);144}145`;146147const file: IRenameScenarioFile = {148kind: 'relativeFile',149fileName: 'fibonacci.ts',150languageId: 'typescript',151fileContents,152isCurrent: true,153};154155const symbols = await provideNewSymbolNames(testingServiceCollection, [file]);156157assert.ok(symbols, `Expected symbols to be non-null`);158assertLength(symbols);159assert.ok(symbols.some(s => s.newSymbolName.startsWith('_')), 'Expected to include symbols with underscore');160assertIncludesLowercased(symbols, ['fib', 'sequence']);161});162163stest('rename a variable reference within a function', async (testingServiceCollection) => {164const fileContents = (outdent`165function fromQueryMatches(matches: Parser.QueryMatch[]): InSourceTreeSitterQuery[] {166const captures = matches.flatMap(({ captures }) => captures)167.sort((a, b) => a.node.startIndex - b.node.startIndex || b.node.endIndex - a.node.endIndex);168169const qs: InSourceTreeSitterQuery[] = [];170for (let i = 0; i < captures.length;) {171const capture = captures[i];172if (capture.name === 'call_expression' && captures[i + 2].name === 'target_language' && captures[i + 3].name === 'query_src_with_quotes') {173<<qs>>.push(new InSourceTreeSitterQuery(captures[i + 2].node, captures[i + 3].node));174i += 4;175} else {176i++;177}178}179180return qs;181}182`);183184const file: IRenameScenarioFile = {185kind: 'relativeFile',186fileName: 'queryDiagnosticsProvider.ts',187languageId: 'typescript',188fileContents,189isCurrent: true,190};191192const symbols = await provideNewSymbolNames(testingServiceCollection, [file]);193194assert.ok(symbols, `Expected symbols to be non-null`);195assertLength(symbols);196assertIncludesLowercased(symbols, ['quer']);197});198199stest('rename a SCREAMING_SNAKE_CASE enum member', async (testingServiceCollection) => {200const fileContents = (outdent`201enum LoadStatus {202NOT_LOADED,203LOADING_FROM_CACHE,204<<LOADING_FROM_SRVER>>,205LOADED,206}207`);208209const file: IRenameScenarioFile = {210kind: 'relativeFile',211fileName: 'queryDiagnosticsProvider.ts',212languageId: 'typescript',213fileContents,214isCurrent: true,215};216217const symbols = await provideNewSymbolNames(testingServiceCollection, [file]);218219assert.ok(symbols, `Expected symbols to be non-null`);220assertLength(symbols);221assert.ok(symbols.every(symbol => guessNamingConvention(symbol.newSymbolName) === NamingConvention.ScreamingSnakeCase), 'Expected all symbols to be SCREAMING_SNAKE_CASE');222});223224stest('respect context: infer name based on existing code - enum member', async (testingServiceCollection) => {225const fileContents = (outdent`226enum Direction {227UP,228DOWN,229RIGHT,230<<TODO>>,231}232`);233234const file: IRenameScenarioFile = {235kind: 'relativeFile',236fileName: 'direction.ts',237languageId: 'typescript',238fileContents,239isCurrent: true,240};241242const symbols = await provideNewSymbolNames(testingServiceCollection, [file]);243244assert.ok(symbols, `Expected symbols to be non-null`);245assertLength(symbols);246assert.ok(symbols.every(symbol => [NamingConvention.Uppercase, NamingConvention.ScreamingSnakeCase].includes(guessNamingConvention(symbol.newSymbolName))), 'Expected all symbols to be SCREAMING_SNAKE_CASE or UPPERCASE');247});248249stest('rename a function call - definition in same file', async (testingServiceCollection) => {250const fileContents = (outdent`251export function f(n: number): number {252if (n <= 1) {253return 1;254}255return f(n - 1) + f(n - 2);256}257258const result = <<f>>(10);259`);260const file: IRenameScenarioFile = {261kind: 'relativeFile',262fileName: 'script.ts',263languageId: 'typescript',264fileContents,265isCurrent: true,266};267268const symbols = await provideNewSymbolNames(testingServiceCollection, [file]);269270assert.ok(symbols && symbols.length > 1, 'Expected to provide > 1 symbols');271assertIncludesLowercased(symbols, ['fib', 'sequence']);272});273274stest('rename a function call - definition in different file', async (testingServiceCollection) => {275const currentFile: IRenameScenarioFile = {276kind: 'relativeFile',277fileName: 'script.ts',278languageId: 'typescript',279fileContents: outdent`280import { f } from './impl';281282const result = <<f>>(10);283`,284isCurrent: true,285};286287const fileWithFnDef: IRelativeFile = {288kind: 'relativeFile',289fileName: 'impl.ts',290languageId: 'typescript',291fileContents: outdent`292export function f(n: number): number {293if (n <= 1) {294return 1;295}296return f(n - 1) + f(n - 2);297}298`,299};300301const symbols = await provideNewSymbolNames(testingServiceCollection, [currentFile, fileWithFnDef]);302303assert.ok(symbols && symbols.length > 1, 'Expected to provide > 1 symbols');304assertIncludesLowercased(symbols, ['fib', 'sequence']);305});306307stest('rename type definition', async (testingServiceCollection) => {308const fileContents = (outdent`309type <<t>> = {310firstName: string;311lastName: string;312}313`);314const file: IRenameScenarioFile = {315kind: 'relativeFile',316fileName: 'script.ts',317languageId: 'typescript',318fileContents,319isCurrent: true,320};321322const symbols = await provideNewSymbolNames(testingServiceCollection, [file]);323324assert.ok(symbols && symbols.length > 1, 'Expected to provide > 1 symbols');325assert.ok(symbols.some(s => s.newSymbolName.toLowerCase().includes('person')), 'Inludes person');326});327328stest('rename type definition when it is used in the same file', async (testingServiceCollection) => {329const fileContents = (outdent`330type <<t>> = {331firstName: string;332lastName: string;333}334335function greet(p: t): string {336return 'Hello ' + p.firstName + ' ' + p.lastName;337}338`);339const file: IRenameScenarioFile = {340kind: 'relativeFile',341fileName: 'script.ts',342languageId: 'typescript',343fileContents,344isCurrent: true,345};346347const symbols = await provideNewSymbolNames(testingServiceCollection, [file]);348349assert.ok(symbols && symbols.length > 1, 'Expected to provide > 1 symbols');350assert.ok(symbols.some(s => s.newSymbolName.toLowerCase().includes('person')), 'Inludes person');351});352353stest('rename type reference - same file', async (testingServiceCollection) => {354const fileContents = (outdent`355type t = {356firstName: string;357lastName: string;358}359360function greet(p: <<t>>): string {361return 'Hello ' + p.firstName + ' ' + p.lastName;362}363`);364const file: IRenameScenarioFile = {365kind: 'relativeFile',366fileName: 'script.ts',367languageId: 'typescript',368fileContents,369isCurrent: true,370};371372const symbols = await provideNewSymbolNames(testingServiceCollection, [file]);373374assert.ok(symbols && symbols.length > 1, 'Expected to provide > 1 symbols');375assert.ok(symbols.some(s => s.newSymbolName.toLowerCase().includes('person')), 'Includes person');376});377378stest('rename type reference - same file with 2 possible defs', async (testingServiceCollection) => {379const fileContents = (outdent`380type t = {381firstName: string;382lastName: string;383}384385const t = {386bar: 1387}388389function greet(p: <<t>>): string {390return 'Hello ' + p.firstName + ' ' + p.lastName;391}392`);393const file: IRenameScenarioFile = {394kind: 'relativeFile',395fileName: 'script.ts',396languageId: 'typescript',397fileContents,398isCurrent: true,399};400401const symbols = await provideNewSymbolNames(testingServiceCollection, [file]);402403assert.ok(symbols && symbols.length > 1, 'Expected to provide > 1 symbols');404assert.ok(symbols.some(s => s.newSymbolName.toLowerCase().includes('person')), 'Includes person');405});406407stest('rename class - same file', async (testingServiceCollection) => {408const fileContents = outdent`409class <<P>> {410firstName: string;411lastName: string;412}413`;414const file: IRenameScenarioFile = {415kind: 'relativeFile',416fileName: 'script.ts',417languageId: 'typescript',418fileContents,419isCurrent: true,420};421422const symbols = await provideNewSymbolNames(testingServiceCollection, [file]);423424assert.ok(symbols && symbols.length > 1, 'Expected to provide > 1 symbols');425assert.ok(symbols.some(s => s.newSymbolName.toLowerCase().includes('person')), 'Inludes person');426});427428stest('rename class reference - same file', async (testingServiceCollection) => {429const fileContents = outdent`430class P {431firstName: string;432lastName: string;433}434435function greet(p: <<P>>): string {436return 'Hello ' + p.firstName + ' ' + p.lastName;437}438`;439const file: IRenameScenarioFile = {440kind: 'relativeFile',441fileName: 'script.ts',442languageId: 'typescript',443fileContents,444isCurrent: true,445};446447const symbols = await provideNewSymbolNames(testingServiceCollection, [file]);448449assert.ok(symbols && symbols.length > 1, 'Expected to provide > 1 symbols');450assert.ok(symbols.some(s => s.newSymbolName.toLowerCase().includes('person')), 'Inludes person');451});452453stest('rename method with field-awareness', async (testingServiceCollection) => {454const fileContents = outdent`455class Processor {456private stdoutBuffer: string = '';457458<<clearBuffer>>() {459// TODO: implement460}461}462`;463const file: IRenameScenarioFile = {464kind: 'relativeFile',465fileName: 'script.ts',466languageId: 'typescript',467fileContents,468isCurrent: true,469};470471const symbols = await provideNewSymbolNames(testingServiceCollection, [file]);472473assert.ok(symbols && symbols.length > 1, 'Expected to provide > 1 symbols');474assert.ok(symbols.some(s => s.newSymbolName.toLowerCase().includes('stdout')), 'Knows about `stdoutBuffer`');475});476477stest('non-tree-sitter language', async (testingServiceCollection) => {478const fileContents = outdent`479let rec <<f>> n = if n <= 1 then 1 else f (n - 1) + f (n - 2)480`;481const file: IRenameScenarioFile = {482kind: 'relativeFile',483fileName: 'impl.ml',484languageId: 'ocaml',485fileContents,486isCurrent: true,487};488489const symbols = await provideNewSymbolNames(testingServiceCollection, [file]);490491assert.ok(symbols && symbols.length > 1, 'Expected to provide > 1 symbols');492assert.ok(symbols.some(s => s.newSymbolName.toLowerCase().includes('fib')), 'Includes fib');493});494495stest('rename class name - CSS', async (testingServiceCollection) => {496const fileContents = outdent`497.box {498background-color: #fff;499}500501<<.button>> {502color: #fff;503background-color: #000;504}505`;506const file: IRenameScenarioFile = {507kind: 'relativeFile',508fileName: 'style.css',509languageId: 'css',510fileContents,511isCurrent: true,512};513514const symbols = await provideNewSymbolNames(testingServiceCollection, [file]);515516assert.ok(symbols && symbols.length > 1, 'Expected to provide > 1 symbols');517assert.ok(symbols.every(s => s.newSymbolName.match(/^\.([a-zA-Z]+)/)), 'All symbols are class names');518});519520});521522523