diff --git a/Sources/SemanticIndex/CheckedIndex.swift b/Sources/SemanticIndex/CheckedIndex.swift index 48d849fee..ec6a99036 100644 --- a/Sources/SemanticIndex/CheckedIndex.swift +++ b/Sources/SemanticIndex/CheckedIndex.swift @@ -355,11 +355,14 @@ private struct IndexOutOfDateChecker { private enum Error: Swift.Error, CustomStringConvertible { case fileAttributesDontHaveModificationDate + case circularSymlink(URL) var description: String { switch self { case .fileAttributesDontHaveModificationDate: return "File attributes don't contain a modification date" + case .circularSymlink(let url): + return "Circular symlink at \(url)" } } } @@ -517,6 +520,8 @@ private struct IndexOutOfDateChecker { } var modificationDate = try Self.modificationDate(atPath: fileURL.filePath) + var visited: Set = [fileURL] + // Get the maximum mtime in the symlink chain as the modification date of the URI. That way if either the symlink // is changed to point to a different file or if the underlying file is modified, the modification time is // updated. @@ -524,6 +529,9 @@ private struct IndexOutOfDateChecker { atPath: fileURL.filePath ) { fileURL = URL(fileURLWithPath: relativeSymlinkDestination, relativeTo: fileURL) + if !visited.insert(fileURL).inserted { + throw Error.circularSymlink(fileURL) + } modificationDate = max(modificationDate, try Self.modificationDate(atPath: fileURL.filePath)) } diff --git a/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift b/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift index 5988cdafa..1aca7fc6c 100644 --- a/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift +++ b/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift @@ -2521,6 +2521,24 @@ final class BackgroundIndexingTests: XCTestCase { let workspaceSymbolsAfterUpdate = try await project.testClient.send(WorkspaceSymbolsRequest(query: "myTest")) XCTAssertEqual(workspaceSymbolsAfterUpdate?.compactMap(\.symbolInformation?.name), ["myTestA_updated", "myTestB"]) } + + func testCircularSymlink() async throws { + let project = try await SwiftPMTestProject( + files: [ + "Symlink.swift": "" + ], + enableBackgroundIndexing: true + ) + let circularSymlink = try XCTUnwrap(project.uri(for: "Symlink.swift").fileURL) + try FileManager.default.removeItem(at: circularSymlink) + try FileManager.default.createSymbolicLink(at: circularSymlink, withDestinationURL: circularSymlink) + + project.testClient.send( + DidChangeWatchedFilesNotification(changes: [FileEvent(uri: URI(circularSymlink), type: .changed)]) + ) + // Check that we don't enter an infinite loop trying to index the circular symlink. + try await project.testClient.send(SynchronizeRequest(index: true)) + } } extension HoverResponseContents {