mirror of
https://github.com/astral-sh/ruff.git
synced 2025-11-02 04:48:07 +00:00
## Summary
Closes: astral-sh/ty#88
This PR implements an initial version of a mock language server that can
be used to write e2e tests using the real server running in the
background.
The way it works is that you'd use the `TestServerBuilder` to help
construct the `TestServer` with the setup data. This could be the
workspace folders, populating the file and it's content in the memory
file system, setting the right client capabilities to make the server
respond correctly, etc. This can be expanded as we write more test
cases.
There are still a few things to follow-up on:
- ~In the `Drop` implementation, we should assert that there are no
pending notification, request and responses from the server that the
test code hasn't handled yet~ Implemented in [`afd1f82`
(#19391)](afd1f82bde)
- Reduce the setup boilerplate in any way we can
- Improve the final assertion, currently I'm just snapshotting the final
output
## Test Plan
Written a few test cases.
323 lines
10 KiB
Rust
323 lines
10 KiB
Rust
use std::any::Any;
|
|
use std::fmt;
|
|
use std::fmt::Display;
|
|
use std::panic::RefUnwindSafe;
|
|
use std::sync::Arc;
|
|
|
|
use lsp_types::Url;
|
|
use ruff_db::file_revision::FileRevision;
|
|
use ruff_db::files::{File, FilePath};
|
|
use ruff_db::system::walk_directory::WalkDirectoryBuilder;
|
|
use ruff_db::system::{
|
|
CaseSensitivity, DirectoryEntry, FileType, GlobError, Metadata, PatternError, Result, System,
|
|
SystemPath, SystemPathBuf, SystemVirtualPath, SystemVirtualPathBuf, WritableSystem,
|
|
};
|
|
use ruff_notebook::{Notebook, NotebookError};
|
|
use ty_python_semantic::Db;
|
|
|
|
use crate::DocumentQuery;
|
|
use crate::document::DocumentKey;
|
|
use crate::session::index::Index;
|
|
|
|
/// Returns a [`Url`] for the given [`File`].
|
|
pub(crate) fn file_to_url(db: &dyn Db, file: File) -> Option<Url> {
|
|
match file.path(db) {
|
|
FilePath::System(system) => Url::from_file_path(system.as_std_path()).ok(),
|
|
FilePath::SystemVirtual(path) => Url::parse(path.as_str()).ok(),
|
|
FilePath::Vendored(path) => {
|
|
let writable = db.system().as_writable()?;
|
|
|
|
let system_path = SystemPathBuf::from(format!(
|
|
"vendored/typeshed/{}/{}",
|
|
// The vendored files are uniquely identified by the source commit.
|
|
ty_vendored::SOURCE_COMMIT,
|
|
path.as_str()
|
|
));
|
|
|
|
// Extract the vendored file onto the system.
|
|
let system_path = writable
|
|
.get_or_cache(&system_path, &|| db.vendored().read_to_string(path))
|
|
.ok()
|
|
.flatten()?;
|
|
|
|
Url::from_file_path(system_path.as_std_path()).ok()
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Represents either a [`SystemPath`] or a [`SystemVirtualPath`].
|
|
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
|
|
pub(crate) enum AnySystemPath {
|
|
System(SystemPathBuf),
|
|
SystemVirtual(SystemVirtualPathBuf),
|
|
}
|
|
|
|
impl AnySystemPath {
|
|
/// Converts the given [`Url`] to an [`AnySystemPath`].
|
|
///
|
|
/// If the URL scheme is `file`, then the path is converted to a [`SystemPathBuf`]. Otherwise, the
|
|
/// URL is converted to a [`SystemVirtualPathBuf`].
|
|
///
|
|
/// This fails in the following cases:
|
|
/// * The URL cannot be converted to a file path (refer to [`Url::to_file_path`]).
|
|
/// * If the URL is not a valid UTF-8 string.
|
|
pub(crate) fn try_from_url(url: &Url) -> std::result::Result<Self, ()> {
|
|
if url.scheme() == "file" {
|
|
Ok(AnySystemPath::System(
|
|
SystemPathBuf::from_path_buf(url.to_file_path()?).map_err(|_| ())?,
|
|
))
|
|
} else {
|
|
Ok(AnySystemPath::SystemVirtual(
|
|
SystemVirtualPath::new(url.as_str()).to_path_buf(),
|
|
))
|
|
}
|
|
}
|
|
|
|
pub(crate) const fn as_system(&self) -> Option<&SystemPathBuf> {
|
|
match self {
|
|
AnySystemPath::System(system_path_buf) => Some(system_path_buf),
|
|
AnySystemPath::SystemVirtual(_) => None,
|
|
}
|
|
}
|
|
|
|
/// Returns the extension of the path, if any.
|
|
pub(crate) fn extension(&self) -> Option<&str> {
|
|
match self {
|
|
AnySystemPath::System(system_path) => system_path.extension(),
|
|
AnySystemPath::SystemVirtual(virtual_path) => virtual_path.extension(),
|
|
}
|
|
}
|
|
|
|
/// Converts the path to a URL.
|
|
pub(crate) fn to_url(&self) -> Option<Url> {
|
|
match self {
|
|
AnySystemPath::System(system_path) => {
|
|
Url::from_file_path(system_path.as_std_path()).ok()
|
|
}
|
|
AnySystemPath::SystemVirtual(virtual_path) => Url::parse(virtual_path.as_str()).ok(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for AnySystemPath {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
AnySystemPath::System(system_path) => write!(f, "{system_path}"),
|
|
AnySystemPath::SystemVirtual(virtual_path) => write!(f, "{virtual_path}"),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub(crate) struct LSPSystem {
|
|
/// A read-only copy of the index where the server stores all the open documents and settings.
|
|
///
|
|
/// This will be [`None`] when a mutable reference is held to the index via [`index_mut`]
|
|
/// method to prevent the index from being accessed while it is being modified. It will be
|
|
/// restored when the mutable reference is dropped.
|
|
///
|
|
/// [`index_mut`]: crate::Session::index_mut
|
|
index: Option<Arc<Index>>,
|
|
|
|
/// A native system implementation.
|
|
///
|
|
/// This is used to delegate method calls that are not handled by the LSP system. It is also
|
|
/// used as a fallback when the documents are not found in the LSP index.
|
|
native_system: Arc<dyn System + 'static + Send + Sync + RefUnwindSafe>,
|
|
}
|
|
|
|
impl LSPSystem {
|
|
pub(crate) fn new(
|
|
index: Arc<Index>,
|
|
native_system: Arc<dyn System + 'static + Send + Sync + RefUnwindSafe>,
|
|
) -> Self {
|
|
Self {
|
|
index: Some(index),
|
|
native_system,
|
|
}
|
|
}
|
|
|
|
/// Takes the index out of the system.
|
|
pub(crate) fn take_index(&mut self) -> Option<Arc<Index>> {
|
|
self.index.take()
|
|
}
|
|
|
|
/// Sets the index for the system.
|
|
pub(crate) fn set_index(&mut self, index: Arc<Index>) {
|
|
self.index = Some(index);
|
|
}
|
|
|
|
/// Returns a reference to the contained index.
|
|
///
|
|
/// # Panics
|
|
///
|
|
/// Panics if the index is `None`.
|
|
fn index(&self) -> &Index {
|
|
self.index.as_ref().unwrap()
|
|
}
|
|
|
|
fn make_document_ref(&self, path: AnySystemPath) -> Option<DocumentQuery> {
|
|
let index = self.index();
|
|
let key = DocumentKey::from_path(path);
|
|
index.make_document_ref(key).ok()
|
|
}
|
|
|
|
fn system_path_to_document_ref(&self, path: &SystemPath) -> Option<DocumentQuery> {
|
|
let any_path = AnySystemPath::System(path.to_path_buf());
|
|
self.make_document_ref(any_path)
|
|
}
|
|
|
|
fn system_virtual_path_to_document_ref(
|
|
&self,
|
|
path: &SystemVirtualPath,
|
|
) -> Option<DocumentQuery> {
|
|
let any_path = AnySystemPath::SystemVirtual(path.to_path_buf());
|
|
self.make_document_ref(any_path)
|
|
}
|
|
}
|
|
|
|
impl System for LSPSystem {
|
|
fn path_metadata(&self, path: &SystemPath) -> Result<Metadata> {
|
|
let document = self.system_path_to_document_ref(path);
|
|
|
|
if let Some(document) = document {
|
|
Ok(Metadata::new(
|
|
document_revision(&document),
|
|
None,
|
|
FileType::File,
|
|
))
|
|
} else {
|
|
self.native_system.path_metadata(path)
|
|
}
|
|
}
|
|
|
|
fn canonicalize_path(&self, path: &SystemPath) -> Result<SystemPathBuf> {
|
|
self.native_system.canonicalize_path(path)
|
|
}
|
|
|
|
fn path_exists_case_sensitive(&self, path: &SystemPath, prefix: &SystemPath) -> bool {
|
|
self.native_system.path_exists_case_sensitive(path, prefix)
|
|
}
|
|
|
|
fn read_to_string(&self, path: &SystemPath) -> Result<String> {
|
|
let document = self.system_path_to_document_ref(path);
|
|
|
|
match document {
|
|
Some(DocumentQuery::Text { document, .. }) => Ok(document.contents().to_string()),
|
|
_ => self.native_system.read_to_string(path),
|
|
}
|
|
}
|
|
|
|
fn read_to_notebook(&self, path: &SystemPath) -> std::result::Result<Notebook, NotebookError> {
|
|
let document = self.system_path_to_document_ref(path);
|
|
|
|
match document {
|
|
Some(DocumentQuery::Text { document, .. }) => {
|
|
Notebook::from_source_code(document.contents())
|
|
}
|
|
Some(DocumentQuery::Notebook { notebook, .. }) => Ok(notebook.make_ruff_notebook()),
|
|
None => self.native_system.read_to_notebook(path),
|
|
}
|
|
}
|
|
|
|
fn read_virtual_path_to_string(&self, path: &SystemVirtualPath) -> Result<String> {
|
|
let document = self
|
|
.system_virtual_path_to_document_ref(path)
|
|
.ok_or_else(|| virtual_path_not_found(path))?;
|
|
|
|
if let DocumentQuery::Text { document, .. } = &document {
|
|
Ok(document.contents().to_string())
|
|
} else {
|
|
Err(not_a_text_document(path))
|
|
}
|
|
}
|
|
|
|
fn read_virtual_path_to_notebook(
|
|
&self,
|
|
path: &SystemVirtualPath,
|
|
) -> std::result::Result<Notebook, NotebookError> {
|
|
let document = self
|
|
.system_virtual_path_to_document_ref(path)
|
|
.ok_or_else(|| virtual_path_not_found(path))?;
|
|
|
|
match document {
|
|
DocumentQuery::Text { document, .. } => Notebook::from_source_code(document.contents()),
|
|
DocumentQuery::Notebook { notebook, .. } => Ok(notebook.make_ruff_notebook()),
|
|
}
|
|
}
|
|
|
|
fn current_directory(&self) -> &SystemPath {
|
|
self.native_system.current_directory()
|
|
}
|
|
|
|
fn user_config_directory(&self) -> Option<SystemPathBuf> {
|
|
self.native_system.user_config_directory()
|
|
}
|
|
|
|
fn cache_dir(&self) -> Option<SystemPathBuf> {
|
|
self.native_system.cache_dir()
|
|
}
|
|
|
|
fn read_directory<'a>(
|
|
&'a self,
|
|
path: &SystemPath,
|
|
) -> Result<Box<dyn Iterator<Item = Result<DirectoryEntry>> + 'a>> {
|
|
self.native_system.read_directory(path)
|
|
}
|
|
|
|
fn walk_directory(&self, path: &SystemPath) -> WalkDirectoryBuilder {
|
|
self.native_system.walk_directory(path)
|
|
}
|
|
|
|
fn glob(
|
|
&self,
|
|
pattern: &str,
|
|
) -> std::result::Result<
|
|
Box<dyn Iterator<Item = std::result::Result<SystemPathBuf, GlobError>> + '_>,
|
|
PatternError,
|
|
> {
|
|
self.native_system.glob(pattern)
|
|
}
|
|
|
|
fn as_writable(&self) -> Option<&dyn WritableSystem> {
|
|
self.native_system.as_writable()
|
|
}
|
|
|
|
fn as_any(&self) -> &dyn Any {
|
|
self
|
|
}
|
|
|
|
fn as_any_mut(&mut self) -> &mut dyn Any {
|
|
self
|
|
}
|
|
|
|
fn case_sensitivity(&self) -> CaseSensitivity {
|
|
self.native_system.case_sensitivity()
|
|
}
|
|
|
|
fn env_var(&self, name: &str) -> std::result::Result<String, std::env::VarError> {
|
|
self.native_system.env_var(name)
|
|
}
|
|
}
|
|
|
|
fn not_a_text_document(path: impl Display) -> std::io::Error {
|
|
std::io::Error::new(
|
|
std::io::ErrorKind::InvalidInput,
|
|
format!("Input is not a text document: {path}"),
|
|
)
|
|
}
|
|
|
|
fn virtual_path_not_found(path: impl Display) -> std::io::Error {
|
|
std::io::Error::new(
|
|
std::io::ErrorKind::NotFound,
|
|
format!("Virtual path does not exist: {path}"),
|
|
)
|
|
}
|
|
|
|
/// Helper function to get the [`FileRevision`] of the given document.
|
|
fn document_revision(document: &DocumentQuery) -> FileRevision {
|
|
// The file revision is just an opaque number which doesn't have any significant meaning other
|
|
// than that the file has changed if the revisions are different.
|
|
#[expect(clippy::cast_sign_loss)]
|
|
FileRevision::new(document.version() as u128)
|
|
}
|