Skip to content

Commit 45b0b36

Browse files
committed
fix: handle invalid gitignore patterns gracefully during codebase indexing
- Add try-catch blocks to handle invalid regex patterns in .gitignore - Parse .gitignore line by line when bulk parsing fails - Skip individual invalid patterns while processing valid ones - Add comprehensive tests for various gitignore scenarios - Fixes #6881
1 parent ad0e33e commit 45b0b36

File tree

2 files changed

+285
-9
lines changed

2 files changed

+285
-9
lines changed

src/services/code-index/__tests__/manager.spec.ts

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@ import { CodeIndexManager } from "../manager"
22
import { CodeIndexServiceFactory } from "../service-factory"
33
import type { MockedClass } from "vitest"
44
import * as path from "path"
5+
import * as fs from "fs/promises"
6+
import ignore from "ignore"
7+
8+
// Mock fs/promises module
9+
vi.mock("fs/promises")
10+
11+
// Mock ignore module
12+
vi.mock("ignore")
513

614
// Mock vscode module
715
vi.mock("vscode", () => {
@@ -581,4 +589,239 @@ describe("CodeIndexManager - handleSettingsChange regression", () => {
581589
consoleErrorSpy.mockRestore()
582590
})
583591
})
592+
593+
describe("gitignore pattern handling", () => {
594+
let mockIgnoreInstance: any
595+
let mockConfigManager: any
596+
let mockCacheManager: any
597+
let mockServiceFactoryInstance: any
598+
599+
beforeEach(() => {
600+
// Reset mocks
601+
vi.clearAllMocks()
602+
603+
// Mock ignore instance
604+
mockIgnoreInstance = {
605+
add: vi.fn(),
606+
ignores: vi.fn(() => false),
607+
}
608+
609+
// Mock the ignore module to return our mock instance
610+
vi.mocked(ignore).mockReturnValue(mockIgnoreInstance)
611+
612+
// Mock config manager
613+
mockConfigManager = {
614+
loadConfiguration: vi.fn().mockResolvedValue({ requiresRestart: false }),
615+
isFeatureConfigured: true,
616+
isFeatureEnabled: true,
617+
getConfig: vi.fn().mockReturnValue({
618+
isConfigured: true,
619+
embedderProvider: "openai",
620+
modelId: "text-embedding-3-small",
621+
openAiOptions: { openAiNativeApiKey: "test-key" },
622+
qdrantUrl: "http://localhost:6333",
623+
qdrantApiKey: "test-key",
624+
searchMinScore: 0.4,
625+
}),
626+
}
627+
;(manager as any)._configManager = mockConfigManager
628+
629+
// Mock cache manager
630+
mockCacheManager = {
631+
initialize: vi.fn(),
632+
clearCacheFile: vi.fn(),
633+
}
634+
;(manager as any)._cacheManager = mockCacheManager
635+
636+
// Mock service factory
637+
mockServiceFactoryInstance = {
638+
createServices: vi.fn().mockReturnValue({
639+
embedder: { embedderInfo: { name: "openai" } },
640+
vectorStore: {},
641+
scanner: {},
642+
fileWatcher: {
643+
onDidStartBatchProcessing: vi.fn(),
644+
onBatchProgressUpdate: vi.fn(),
645+
watch: vi.fn(),
646+
stopWatcher: vi.fn(),
647+
dispose: vi.fn(),
648+
},
649+
}),
650+
validateEmbedder: vi.fn().mockResolvedValue({ valid: true }),
651+
}
652+
MockedCodeIndexServiceFactory.mockImplementation(() => mockServiceFactoryInstance as any)
653+
})
654+
655+
it("should handle invalid gitignore patterns gracefully", async () => {
656+
// Arrange - Mock .gitignore with invalid pattern
657+
const invalidGitignoreContent = `
658+
# Valid patterns
659+
node_modules/
660+
*.log
661+
662+
# Invalid pattern - character range out of order
663+
pqh[A-/]
664+
665+
# More valid patterns
666+
dist/
667+
.env
668+
`
669+
;(fs.readFile as any).mockResolvedValue(invalidGitignoreContent)
670+
671+
// Make the first add() call throw an error (simulating invalid pattern)
672+
let addCallCount = 0
673+
mockIgnoreInstance.add.mockImplementation((pattern: string) => {
674+
addCallCount++
675+
// Throw on first call (full content), succeed on individual patterns
676+
if (addCallCount === 1) {
677+
throw new Error(
678+
"Invalid regular expression: /^pqh[A-\\/](?=$|\\/$)/i: Range out of order in character class",
679+
)
680+
}
681+
// Throw on the specific invalid pattern
682+
if (pattern.includes("pqh[A-/]")) {
683+
throw new Error(
684+
"Invalid regular expression: /^pqh[A-\\/](?=$|\\/$)/i: Range out of order in character class",
685+
)
686+
}
687+
})
688+
689+
// Spy on console methods
690+
const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {})
691+
692+
// Act
693+
await (manager as any)._recreateServices()
694+
695+
// Assert - Should have logged warnings
696+
expect(consoleWarnSpy).toHaveBeenCalledWith(
697+
expect.stringContaining("Warning: .gitignore contains invalid patterns"),
698+
)
699+
expect(consoleWarnSpy).toHaveBeenCalledWith(
700+
expect.stringContaining('Skipping invalid .gitignore pattern: "pqh[A-/]"'),
701+
)
702+
703+
// Should have attempted to add valid patterns individually
704+
expect(mockIgnoreInstance.add).toHaveBeenCalled()
705+
706+
// Should not throw an error - service creation should continue
707+
expect(mockServiceFactoryInstance.createServices).toHaveBeenCalled()
708+
expect(mockServiceFactoryInstance.validateEmbedder).toHaveBeenCalled()
709+
710+
// Cleanup
711+
consoleWarnSpy.mockRestore()
712+
})
713+
714+
it("should process valid gitignore patterns normally", async () => {
715+
// Arrange - Mock .gitignore with all valid patterns
716+
const validGitignoreContent = `
717+
# Valid patterns
718+
node_modules/
719+
*.log
720+
dist/
721+
.env
722+
`
723+
;(fs.readFile as any).mockResolvedValue(validGitignoreContent)
724+
725+
// All add() calls succeed
726+
mockIgnoreInstance.add.mockImplementation(() => {})
727+
728+
// Spy on console methods
729+
const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {})
730+
731+
// Act
732+
await (manager as any)._recreateServices()
733+
734+
// Assert - Should not have logged any warnings
735+
expect(consoleWarnSpy).not.toHaveBeenCalled()
736+
737+
// Should have added the content and .gitignore itself
738+
expect(mockIgnoreInstance.add).toHaveBeenCalledWith(validGitignoreContent)
739+
expect(mockIgnoreInstance.add).toHaveBeenCalledWith(".gitignore")
740+
741+
// Service creation should proceed normally
742+
expect(mockServiceFactoryInstance.createServices).toHaveBeenCalled()
743+
expect(mockServiceFactoryInstance.validateEmbedder).toHaveBeenCalled()
744+
745+
// Cleanup
746+
consoleWarnSpy.mockRestore()
747+
})
748+
749+
it("should handle missing .gitignore file gracefully", async () => {
750+
// Arrange - Mock file not found error
751+
;(fs.readFile as any).mockRejectedValue(new Error("ENOENT: no such file or directory"))
752+
753+
// Spy on console methods
754+
const consoleInfoSpy = vi.spyOn(console, "info").mockImplementation(() => {})
755+
756+
// Act
757+
await (manager as any)._recreateServices()
758+
759+
// Assert - Should log info message
760+
expect(consoleInfoSpy).toHaveBeenCalledWith(
761+
".gitignore file not found or could not be read, proceeding without gitignore patterns",
762+
)
763+
764+
// Should not attempt to add patterns
765+
expect(mockIgnoreInstance.add).not.toHaveBeenCalled()
766+
767+
// Service creation should proceed normally
768+
expect(mockServiceFactoryInstance.createServices).toHaveBeenCalled()
769+
expect(mockServiceFactoryInstance.validateEmbedder).toHaveBeenCalled()
770+
771+
// Cleanup
772+
consoleInfoSpy.mockRestore()
773+
})
774+
775+
it("should handle mixed valid and invalid patterns", async () => {
776+
// Arrange - Mock .gitignore with mix of valid and invalid patterns
777+
const mixedGitignoreContent = `
778+
node_modules/
779+
pqh[A-/]
780+
*.log
781+
[Z-A]invalid
782+
dist/
783+
`
784+
;(fs.readFile as any).mockResolvedValue(mixedGitignoreContent)
785+
786+
// Make add() throw on invalid patterns
787+
mockIgnoreInstance.add.mockImplementation((pattern: string) => {
788+
if (pattern === mixedGitignoreContent) {
789+
throw new Error("Invalid patterns detected")
790+
}
791+
if (pattern.includes("[A-/]") || pattern.includes("[Z-A]")) {
792+
throw new Error("Invalid character range")
793+
}
794+
})
795+
796+
// Spy on console methods
797+
const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {})
798+
799+
// Act
800+
await (manager as any)._recreateServices()
801+
802+
// Assert - Should have logged warnings for invalid patterns
803+
expect(consoleWarnSpy).toHaveBeenCalledWith(
804+
expect.stringContaining("Warning: .gitignore contains invalid patterns"),
805+
)
806+
expect(consoleWarnSpy).toHaveBeenCalledWith(
807+
expect.stringContaining('Skipping invalid .gitignore pattern: "pqh[A-/]"'),
808+
)
809+
expect(consoleWarnSpy).toHaveBeenCalledWith(
810+
expect.stringContaining('Skipping invalid .gitignore pattern: "[Z-A]invalid"'),
811+
)
812+
813+
// Should have attempted to add valid patterns
814+
expect(mockIgnoreInstance.add).toHaveBeenCalledWith("node_modules/")
815+
expect(mockIgnoreInstance.add).toHaveBeenCalledWith("*.log")
816+
expect(mockIgnoreInstance.add).toHaveBeenCalledWith("dist/")
817+
expect(mockIgnoreInstance.add).toHaveBeenCalledWith(".gitignore")
818+
819+
// Service creation should proceed normally
820+
expect(mockServiceFactoryInstance.createServices).toHaveBeenCalled()
821+
expect(mockServiceFactoryInstance.validateEmbedder).toHaveBeenCalled()
822+
823+
// Cleanup
824+
consoleWarnSpy.mockRestore()
825+
})
826+
})
584827
})

src/services/code-index/manager.ts

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -315,16 +315,49 @@ export class CodeIndexManager {
315315
const ignorePath = path.join(workspacePath, ".gitignore")
316316
try {
317317
const content = await fs.readFile(ignorePath, "utf8")
318-
ignoreInstance.add(content)
319-
ignoreInstance.add(".gitignore")
318+
319+
// Try to add the gitignore patterns, but handle invalid regex patterns gracefully
320+
try {
321+
ignoreInstance.add(content)
322+
ignoreInstance.add(".gitignore")
323+
} catch (ignoreError) {
324+
// Log warning about invalid patterns but continue with indexing
325+
console.warn(
326+
`Warning: .gitignore contains invalid patterns that could not be parsed. Some files may not be properly ignored during indexing. Error: ${
327+
ignoreError instanceof Error ? ignoreError.message : String(ignoreError)
328+
}`,
329+
)
330+
331+
// Try to add individual lines to identify and skip problematic patterns
332+
const lines = content.split("\n")
333+
for (const line of lines) {
334+
const trimmedLine = line.trim()
335+
// Skip empty lines and comments
336+
if (!trimmedLine || trimmedLine.startsWith("#")) {
337+
continue
338+
}
339+
340+
try {
341+
// Create a new ignore instance to test each pattern
342+
const testIgnore = ignore()
343+
testIgnore.add(trimmedLine)
344+
// If successful, add to the main instance
345+
ignoreInstance.add(trimmedLine)
346+
} catch (lineError) {
347+
console.warn(`Skipping invalid .gitignore pattern: "${trimmedLine}"`)
348+
}
349+
}
350+
351+
// Always add .gitignore itself to the ignore list
352+
try {
353+
ignoreInstance.add(".gitignore")
354+
} catch {
355+
// Even this basic pattern failed, but continue anyway
356+
}
357+
}
320358
} catch (error) {
321-
// Should never happen: reading file failed even though it exists
322-
console.error("Unexpected error loading .gitignore:", error)
323-
TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
324-
error: error instanceof Error ? error.message : String(error),
325-
stack: error instanceof Error ? error.stack : undefined,
326-
location: "_recreateServices",
327-
})
359+
// File reading error - .gitignore might not exist or be inaccessible
360+
console.info(".gitignore file not found or could not be read, proceeding without gitignore patterns")
328361
}
329362

330363
// (Re)Create shared service instances

0 commit comments

Comments
 (0)