diff --git a/src/tools/rust-analyzer/crates/vfs-notify/src/lib.rs b/src/tools/rust-analyzer/crates/vfs-notify/src/lib.rs index 4cfdec2b5c5d7..45bb777d4d2b2 100644 --- a/src/tools/rust-analyzer/crates/vfs-notify/src/lib.rs +++ b/src/tools/rust-analyzer/crates/vfs-notify/src/lib.rs @@ -9,7 +9,10 @@ #![warn(rust_2018_idioms, unused_lifetimes)] -use std::fs; +use std::{ + fs, + path::{Component, Path}, +}; use crossbeam_channel::{never, select, unbounded, Receiver, Sender}; use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher}; @@ -206,6 +209,11 @@ impl NotifyActor { return true; } let path = entry.path(); + + if path_is_parent_symlink(path) { + return false; + } + root == path || dirs.exclude.iter().chain(&dirs.include).all(|it| it != path) }); @@ -258,3 +266,21 @@ fn read(path: &AbsPath) -> Option> { fn log_notify_error(res: notify::Result) -> Option { res.map_err(|err| tracing::warn!("notify error: {}", err)).ok() } + +/// Is `path` a symlink to a parent directory? +/// +/// Including this path is guaranteed to cause an infinite loop. This +/// heuristic is not sufficient to catch all symlink cycles (it's +/// possible to construct cycle using two or more symlinks), but it +/// catches common cases. +fn path_is_parent_symlink(path: &Path) -> bool { + let Ok(destination) = std::fs::read_link(path) else { + return false; + }; + + // If the symlink is of the form "../..", it's a parent symlink. + let is_relative_parent = + destination.components().all(|c| matches!(c, Component::CurDir | Component::ParentDir)); + + is_relative_parent || path.starts_with(destination) +}