mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-13 05:12:55 -07:00
Fix issues with rip grep (#18756)
This commit is contained in:
committed by
GitHub
parent
ef02cec2cd
commit
2eb1c92347
@@ -11,6 +11,7 @@ import * as os from 'node:os';
|
|||||||
import { RipGrepTool } from '../packages/core/src/tools/ripGrep.js';
|
import { RipGrepTool } from '../packages/core/src/tools/ripGrep.js';
|
||||||
import { Config } from '../packages/core/src/config/config.js';
|
import { Config } from '../packages/core/src/config/config.js';
|
||||||
import { WorkspaceContext } from '../packages/core/src/utils/workspaceContext.js';
|
import { WorkspaceContext } from '../packages/core/src/utils/workspaceContext.js';
|
||||||
|
import { createMockMessageBus } from '../packages/core/src/test-utils/mock-message-bus.js';
|
||||||
|
|
||||||
// Mock Config to provide necessary context
|
// Mock Config to provide necessary context
|
||||||
class MockConfig {
|
class MockConfig {
|
||||||
@@ -66,7 +67,7 @@ describe('ripgrep-real-direct', () => {
|
|||||||
await fs.writeFile(path.join(tempDir, 'file3.txt'), 'goodbye moon\n');
|
await fs.writeFile(path.join(tempDir, 'file3.txt'), 'goodbye moon\n');
|
||||||
|
|
||||||
const config = new MockConfig(tempDir) as unknown as Config;
|
const config = new MockConfig(tempDir) as unknown as Config;
|
||||||
tool = new RipGrepTool(config);
|
tool = new RipGrepTool(config, createMockMessageBus());
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
@@ -108,4 +109,24 @@ describe('ripgrep-real-direct', () => {
|
|||||||
expect(result.llmContent).toContain('script.js');
|
expect(result.llmContent).toContain('script.js');
|
||||||
expect(result.llmContent).not.toContain('file1.txt');
|
expect(result.llmContent).not.toContain('file1.txt');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should support context parameters', async () => {
|
||||||
|
// Create a file with multiple lines
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(tempDir, 'context.txt'),
|
||||||
|
'line1\nline2\nline3 match\nline4\nline5\n',
|
||||||
|
);
|
||||||
|
|
||||||
|
const invocation = tool.build({
|
||||||
|
pattern: 'match',
|
||||||
|
context: 1,
|
||||||
|
});
|
||||||
|
const result = await invocation.execute(new AbortController().signal);
|
||||||
|
|
||||||
|
expect(result.llmContent).toContain('Found 1 match');
|
||||||
|
expect(result.llmContent).toContain('context.txt');
|
||||||
|
expect(result.llmContent).toContain('L2- line2');
|
||||||
|
expect(result.llmContent).toContain('L3: line3 match');
|
||||||
|
expect(result.llmContent).toContain('L4- line4');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1408,42 +1408,45 @@ describe('RipGrepTool', () => {
|
|||||||
expect(result.llmContent).toContain('L1: HELLO world');
|
expect(result.llmContent).toContain('L1: HELLO world');
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it('should handle fixed_strings parameter', async () => {
|
||||||
{
|
mockSpawn.mockImplementationOnce(
|
||||||
name: 'fixed_strings parameter',
|
createMockSpawn({
|
||||||
params: { pattern: 'hello.world', fixed_strings: true },
|
outputData:
|
||||||
mockOutput: {
|
JSON.stringify({
|
||||||
path: { text: 'fileA.txt' },
|
type: 'match',
|
||||||
line_number: 1,
|
data: {
|
||||||
lines: { text: 'hello.world\n' },
|
path: { text: 'fileA.txt' },
|
||||||
},
|
line_number: 1,
|
||||||
expectedArgs: ['--fixed-strings'],
|
lines: { text: 'hello.world\n' },
|
||||||
expectedPattern: 'hello.world',
|
},
|
||||||
},
|
}) + '\n',
|
||||||
])(
|
exitCode: 0,
|
||||||
'should handle $name',
|
}),
|
||||||
async ({ params, mockOutput, expectedArgs, expectedPattern }) => {
|
);
|
||||||
mockSpawn.mockImplementationOnce(
|
|
||||||
createMockSpawn({
|
|
||||||
outputData:
|
|
||||||
JSON.stringify({ type: 'match', data: mockOutput }) + '\n',
|
|
||||||
exitCode: 0,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const invocation = grepTool.build(params);
|
const invocation = grepTool.build({
|
||||||
const result = await invocation.execute(abortSignal);
|
pattern: 'hello.world',
|
||||||
|
fixed_strings: true,
|
||||||
|
});
|
||||||
|
const result = await invocation.execute(abortSignal);
|
||||||
|
|
||||||
expect(mockSpawn).toHaveBeenLastCalledWith(
|
expect(mockSpawn).toHaveBeenLastCalledWith(
|
||||||
expect.anything(),
|
expect.anything(),
|
||||||
expect.arrayContaining(expectedArgs),
|
expect.arrayContaining(['--fixed-strings']),
|
||||||
expect.anything(),
|
expect.anything(),
|
||||||
);
|
);
|
||||||
expect(result.llmContent).toContain(
|
expect(result.llmContent).toContain(
|
||||||
`Found 1 match for pattern "${expectedPattern}"`,
|
'Found 1 match for pattern "hello.world"',
|
||||||
);
|
);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
it('should allow invalid regex patterns when fixed_strings is true', () => {
|
||||||
|
const params: RipGrepToolParams = {
|
||||||
|
pattern: '[[',
|
||||||
|
fixed_strings: true,
|
||||||
|
};
|
||||||
|
expect(grepTool.validateToolParams(params)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
it('should handle no_ignore parameter', async () => {
|
it('should handle no_ignore parameter', async () => {
|
||||||
mockSpawn.mockImplementationOnce(
|
mockSpawn.mockImplementationOnce(
|
||||||
@@ -1681,19 +1684,42 @@ describe('RipGrepTool', () => {
|
|||||||
mockSpawn.mockImplementationOnce(
|
mockSpawn.mockImplementationOnce(
|
||||||
createMockSpawn({
|
createMockSpawn({
|
||||||
outputData:
|
outputData:
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'context',
|
||||||
|
data: {
|
||||||
|
path: { text: 'fileA.txt' },
|
||||||
|
line_number: 1,
|
||||||
|
lines: { text: 'hello world\n' },
|
||||||
|
},
|
||||||
|
}) +
|
||||||
|
'\n' +
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
type: 'match',
|
type: 'match',
|
||||||
data: {
|
data: {
|
||||||
path: { text: 'fileA.txt' },
|
path: { text: 'fileA.txt' },
|
||||||
line_number: 2,
|
line_number: 2,
|
||||||
lines: { text: 'second line with world\n' },
|
lines: { text: 'second line with world\n' },
|
||||||
lines_before: [{ text: 'hello world\n' }],
|
|
||||||
lines_after: [
|
|
||||||
{ text: 'third line\n' },
|
|
||||||
{ text: 'fourth line\n' },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
}) + '\n',
|
}) +
|
||||||
|
'\n' +
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'context',
|
||||||
|
data: {
|
||||||
|
path: { text: 'fileA.txt' },
|
||||||
|
line_number: 3,
|
||||||
|
lines: { text: 'third line\n' },
|
||||||
|
},
|
||||||
|
}) +
|
||||||
|
'\n' +
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'context',
|
||||||
|
data: {
|
||||||
|
path: { text: 'fileA.txt' },
|
||||||
|
line_number: 4,
|
||||||
|
lines: { text: 'fourth line\n' },
|
||||||
|
},
|
||||||
|
}) +
|
||||||
|
'\n',
|
||||||
exitCode: 0,
|
exitCode: 0,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -1721,9 +1747,10 @@ describe('RipGrepTool', () => {
|
|||||||
);
|
);
|
||||||
expect(result.llmContent).toContain('Found 1 match for pattern "world"');
|
expect(result.llmContent).toContain('Found 1 match for pattern "world"');
|
||||||
expect(result.llmContent).toContain('File: fileA.txt');
|
expect(result.llmContent).toContain('File: fileA.txt');
|
||||||
|
expect(result.llmContent).toContain('L1- hello world');
|
||||||
expect(result.llmContent).toContain('L2: second line with world');
|
expect(result.llmContent).toContain('L2: second line with world');
|
||||||
// Note: Ripgrep JSON output for context lines doesn't include line numbers for context lines directly
|
expect(result.llmContent).toContain('L3- third line');
|
||||||
// The current parsing only extracts the matched line, so we only assert on that.
|
expect(result.llmContent).toContain('L4- fourth line');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ interface GrepMatch {
|
|||||||
filePath: string;
|
filePath: string;
|
||||||
lineNumber: number;
|
lineNumber: number;
|
||||||
line: string;
|
line: string;
|
||||||
|
isContext?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class GrepToolInvocation extends BaseToolInvocation<
|
class GrepToolInvocation extends BaseToolInvocation<
|
||||||
@@ -267,8 +268,6 @@ class GrepToolInvocation extends BaseToolInvocation<
|
|||||||
return { llmContent: noMatchMsg, returnDisplay: `No matches found` };
|
return { llmContent: noMatchMsg, returnDisplay: `No matches found` };
|
||||||
}
|
}
|
||||||
|
|
||||||
const wasTruncated = allMatches.length >= totalMaxMatches;
|
|
||||||
|
|
||||||
const matchesByFile = allMatches.reduce(
|
const matchesByFile = allMatches.reduce(
|
||||||
(acc, match) => {
|
(acc, match) => {
|
||||||
const fileKey = match.filePath;
|
const fileKey = match.filePath;
|
||||||
@@ -282,16 +281,19 @@ class GrepToolInvocation extends BaseToolInvocation<
|
|||||||
{} as Record<string, GrepMatch[]>,
|
{} as Record<string, GrepMatch[]>,
|
||||||
);
|
);
|
||||||
|
|
||||||
const matchCount = allMatches.length;
|
const matchesOnly = allMatches.filter((m) => !m.isContext);
|
||||||
|
const matchCount = matchesOnly.length;
|
||||||
const matchTerm = matchCount === 1 ? 'match' : 'matches';
|
const matchTerm = matchCount === 1 ? 'match' : 'matches';
|
||||||
|
|
||||||
|
const wasTruncated = matchCount >= totalMaxMatches;
|
||||||
|
|
||||||
let llmContent = `Found ${matchCount} ${matchTerm} for pattern "${this.params.pattern}" ${searchLocationDescription}${this.params.include ? ` (filter: "${this.params.include}")` : ''}${wasTruncated ? ` (results limited to ${totalMaxMatches} matches for performance)` : ''}:\n---\n`;
|
let llmContent = `Found ${matchCount} ${matchTerm} for pattern "${this.params.pattern}" ${searchLocationDescription}${this.params.include ? ` (filter: "${this.params.include}")` : ''}${wasTruncated ? ` (results limited to ${totalMaxMatches} matches for performance)` : ''}:\n---\n`;
|
||||||
|
|
||||||
for (const filePath in matchesByFile) {
|
for (const filePath in matchesByFile) {
|
||||||
llmContent += `File: ${filePath}\n`;
|
llmContent += `File: ${filePath}\n`;
|
||||||
matchesByFile[filePath].forEach((match) => {
|
matchesByFile[filePath].forEach((match) => {
|
||||||
const trimmedLine = match.line.trim();
|
const separator = match.isContext ? '-' : ':';
|
||||||
llmContent += `L${match.lineNumber}: ${trimmedLine}\n`;
|
llmContent += `L${match.lineNumber}${separator} ${match.line}\n`;
|
||||||
});
|
});
|
||||||
llmContent += '---\n';
|
llmContent += '---\n';
|
||||||
}
|
}
|
||||||
@@ -402,11 +404,15 @@ class GrepToolInvocation extends BaseToolInvocation<
|
|||||||
allowedExitCodes: [0, 1],
|
allowedExitCodes: [0, 1],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let matchesFound = 0;
|
||||||
for await (const line of generator) {
|
for await (const line of generator) {
|
||||||
const match = this.parseRipgrepJsonLine(line, absolutePath);
|
const match = this.parseRipgrepJsonLine(line, absolutePath);
|
||||||
if (match) {
|
if (match) {
|
||||||
results.push(match);
|
results.push(match);
|
||||||
if (results.length >= maxMatches) {
|
if (!match.isContext) {
|
||||||
|
matchesFound++;
|
||||||
|
}
|
||||||
|
if (matchesFound >= maxMatches) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -425,11 +431,11 @@ class GrepToolInvocation extends BaseToolInvocation<
|
|||||||
): GrepMatch | null {
|
): GrepMatch | null {
|
||||||
try {
|
try {
|
||||||
const json = JSON.parse(line);
|
const json = JSON.parse(line);
|
||||||
if (json.type === 'match') {
|
if (json.type === 'match' || json.type === 'context') {
|
||||||
const match = json.data;
|
const data = json.data;
|
||||||
// Defensive check: ensure text properties exist (skips binary/invalid encoding)
|
// Defensive check: ensure text properties exist (skips binary/invalid encoding)
|
||||||
if (match.path?.text && match.lines?.text) {
|
if (data.path?.text && data.lines?.text) {
|
||||||
const absoluteFilePath = path.resolve(basePath, match.path.text);
|
const absoluteFilePath = path.resolve(basePath, data.path.text);
|
||||||
const relativeCheck = path.relative(basePath, absoluteFilePath);
|
const relativeCheck = path.relative(basePath, absoluteFilePath);
|
||||||
if (
|
if (
|
||||||
relativeCheck === '..' ||
|
relativeCheck === '..' ||
|
||||||
@@ -443,8 +449,9 @@ class GrepToolInvocation extends BaseToolInvocation<
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
filePath: relativeFilePath || path.basename(absoluteFilePath),
|
filePath: relativeFilePath || path.basename(absoluteFilePath),
|
||||||
lineNumber: match.line_number,
|
lineNumber: data.line_number,
|
||||||
line: match.lines.text.trimEnd(),
|
line: data.lines.text.trimEnd(),
|
||||||
|
isContext: json.type === 'context',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -573,10 +580,12 @@ export class RipGrepTool extends BaseDeclarativeTool<
|
|||||||
protected override validateToolParamValues(
|
protected override validateToolParamValues(
|
||||||
params: RipGrepToolParams,
|
params: RipGrepToolParams,
|
||||||
): string | null {
|
): string | null {
|
||||||
try {
|
if (!params.fixed_strings) {
|
||||||
new RegExp(params.pattern);
|
try {
|
||||||
} catch (error) {
|
new RegExp(params.pattern);
|
||||||
return `Invalid regular expression pattern provided: ${params.pattern}. Error: ${getErrorMessage(error)}`;
|
} catch (error) {
|
||||||
|
return `Invalid regular expression pattern provided: ${params.pattern}. Error: ${getErrorMessage(error)}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only validate path if one is provided
|
// Only validate path if one is provided
|
||||||
|
|||||||
Reference in New Issue
Block a user