Skip to content

Commit 8fea9fa

Browse files
authored
Merge pull request #1572 from ahoppen/mtime-symlink-chain
2 parents 1dcfd76 + c52d5f4 commit 8fea9fa

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
@@ -1420,6 +1420,67 @@ final class BackgroundIndexingTests: XCTestCase {
14201420
// also testing that we don't wait for type checking of Test.swift to finish.
14211421
XCTAssert(Date().timeIntervalSince(dateStarted) < 30)
14221422
}
1423+
1424+
func testRedirectSymlink() async throws {
1425+
let project = try await SwiftPMTestProject(
1426+
files: [
1427+
"/original.swift": """
1428+
func original() {
1429+
foo()
1430+
}
1431+
""",
1432+
"/updated.swift": """
1433+
func updated() {
1434+
foo()
1435+
}
1436+
""",
1437+
"test.swift": """
1438+
func 1️⃣foo() {}
1439+
""",
1440+
],
1441+
workspaces: { scratchDirectory in
1442+
let symlink =
1443+
scratchDirectory
1444+
.appendingPathComponent("Sources")
1445+
.appendingPathComponent("MyLibrary")
1446+
.appendingPathComponent("symlink.swift")
1447+
try FileManager.default.createSymbolicLink(
1448+
at: symlink,
1449+
withDestinationURL: scratchDirectory.appendingPathComponent("original.swift")
1450+
)
1451+
return [WorkspaceFolder(uri: DocumentURI(scratchDirectory))]
1452+
},
1453+
enableBackgroundIndexing: true
1454+
)
1455+
1456+
let (uri, positions) = try project.openDocument("test.swift")
1457+
1458+
let prepare = try await project.testClient.send(
1459+
CallHierarchyPrepareRequest(textDocument: TextDocumentIdentifier(uri), position: positions["1️⃣"])
1460+
)
1461+
let initialItem = try XCTUnwrap(prepare?.only)
1462+
let callsBeforeRedirect = try await project.testClient.send(CallHierarchyIncomingCallsRequest(item: initialItem))
1463+
XCTAssertEqual(callsBeforeRedirect?.only?.from.name, "original()")
1464+
1465+
let symlink =
1466+
project.scratchDirectory
1467+
.appendingPathComponent("Sources")
1468+
.appendingPathComponent("MyLibrary")
1469+
.appendingPathComponent("symlink.swift")
1470+
try FileManager.default.removeItem(at: symlink)
1471+
try FileManager.default.createSymbolicLink(
1472+
at: symlink,
1473+
withDestinationURL: project.scratchDirectory.appendingPathComponent("updated.swift")
1474+
)
1475+
1476+
project.testClient.send(
1477+
DidChangeWatchedFilesNotification(changes: [FileEvent(uri: DocumentURI(symlink), type: .changed)])
1478+
)
1479+
try await project.testClient.send(PollIndexRequest())
1480+
1481+
let callsAfterRedirect = try await project.testClient.send(CallHierarchyIncomingCallsRequest(item: initialItem))
1482+
XCTAssertEqual(callsAfterRedirect?.only?.from.name, "updated()")
1483+
}
14231484
}
14241485

14251486
extension HoverResponseContents {

0 commit comments

Comments
 (0)