Skip to content

Commit c52d5f4

Browse files
committed
Take the maximum modification date in a symlink change as the mtime of the file
rdar://127475248
1 parent ef2dc17 commit c52d5f4

File tree

2 files changed

+81
-4
lines changed

2 files changed

+81
-4
lines changed

Sources/SemanticIndex/CheckedIndex.swift

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -326,15 +326,31 @@ private struct IndexOutOfDateChecker {
326326
}
327327
}
328328

329+
private static func modificationDate(atPath path: String) throws -> Date {
330+
let attributes = try FileManager.default.attributesOfItem(atPath: path)
331+
guard let modificationDate = attributes[FileAttributeKey.modificationDate] as? Date else {
332+
throw Error.fileAttributesDontHaveModificationDate
333+
}
334+
return modificationDate
335+
}
336+
329337
private func modificationDateUncached(of uri: DocumentURI) throws -> ModificationTime {
330338
do {
331-
guard let fileURL = uri.fileURL else {
339+
guard var fileURL = uri.fileURL else {
332340
return .fileDoesNotExist
333341
}
334-
let attributes = try FileManager.default.attributesOfItem(atPath: fileURL.resolvingSymlinksInPath().path)
335-
guard let modificationDate = attributes[FileAttributeKey.modificationDate] as? Date else {
336-
throw Error.fileAttributesDontHaveModificationDate
342+
var modificationDate = try Self.modificationDate(atPath: fileURL.path)
343+
344+
// Get the maximum mtime in the symlink chain as the modification date of the URI. That way if either the symlink
345+
// is changed to point to a different file or if the underlying file is modified, the modification time is
346+
// updated.
347+
while let relativeSymlinkDestination = try? FileManager.default.destinationOfSymbolicLink(atPath: fileURL.path),
348+
let symlinkDestination = URL(string: relativeSymlinkDestination, relativeTo: fileURL)
349+
{
350+
fileURL = symlinkDestination
351+
modificationDate = max(modificationDate, try Self.modificationDate(atPath: fileURL.path))
337352
}
353+
338354
return .date(modificationDate)
339355
} catch let error as NSError where error.domain == NSCocoaErrorDomain && error.code == NSFileReadNoSuchFileError {
340356
return .fileDoesNotExist

Tests/SourceKitLSPTests/BackgroundIndexingTests.swift

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1371,6 +1371,67 @@ final class BackgroundIndexingTests: XCTestCase {
13711371
// also testing that we don't wait for type checking of Test.swift to finish.
13721372
XCTAssert(Date().timeIntervalSince(dateStarted) < 30)
13731373
}
1374+
1375+
func testRedirectSymlink() async throws {
1376+
let project = try await SwiftPMTestProject(
1377+
files: [
1378+
"/original.swift": """
1379+
func original() {
1380+
foo()
1381+
}
1382+
""",
1383+
"/updated.swift": """
1384+
func updated() {
1385+
foo()
1386+
}
1387+
""",
1388+
"test.swift": """
1389+
func 1️⃣foo() {}
1390+
""",
1391+
],
1392+
workspaces: { scratchDirectory in
1393+
let symlink =
1394+
scratchDirectory
1395+
.appendingPathComponent("Sources")
1396+
.appendingPathComponent("MyLibrary")
1397+
.appendingPathComponent("symlink.swift")
1398+
try FileManager.default.createSymbolicLink(
1399+
at: symlink,
1400+
withDestinationURL: scratchDirectory.appendingPathComponent("original.swift")
1401+
)
1402+
return [WorkspaceFolder(uri: DocumentURI(scratchDirectory))]
1403+
},
1404+
enableBackgroundIndexing: true
1405+
)
1406+
1407+
let (uri, positions) = try project.openDocument("test.swift")
1408+
1409+
let prepare = try await project.testClient.send(
1410+
CallHierarchyPrepareRequest(textDocument: TextDocumentIdentifier(uri), position: positions["1️⃣"])
1411+
)
1412+
let initialItem = try XCTUnwrap(prepare?.only)
1413+
let callsBeforeRedirect = try await project.testClient.send(CallHierarchyIncomingCallsRequest(item: initialItem))
1414+
XCTAssertEqual(callsBeforeRedirect?.only?.from.name, "original()")
1415+
1416+
let symlink =
1417+
project.scratchDirectory
1418+
.appendingPathComponent("Sources")
1419+
.appendingPathComponent("MyLibrary")
1420+
.appendingPathComponent("symlink.swift")
1421+
try FileManager.default.removeItem(at: symlink)
1422+
try FileManager.default.createSymbolicLink(
1423+
at: symlink,
1424+
withDestinationURL: project.scratchDirectory.appendingPathComponent("updated.swift")
1425+
)
1426+
1427+
project.testClient.send(
1428+
DidChangeWatchedFilesNotification(changes: [FileEvent(uri: DocumentURI(symlink), type: .changed)])
1429+
)
1430+
try await project.testClient.send(PollIndexRequest())
1431+
1432+
let callsAfterRedirect = try await project.testClient.send(CallHierarchyIncomingCallsRequest(item: initialItem))
1433+
XCTAssertEqual(callsAfterRedirect?.only?.from.name, "updated()")
1434+
}
13741435
}
13751436

13761437
extension HoverResponseContents {

0 commit comments

Comments
 (0)