mirror of
https://github.com/astral-sh/ruff.git
synced 2026-10-28 10:45:37 +08:00
Avoid running uv format in untrusted workspaces (#28873)
Summary -- This PR adds the `--untrusted-workspace` flag to the server CLI, which informs the server that it's running in an untrusted workspace. When this is the case, we fall back on the internal formatting backend instead of using `uv format` to avoid reading uv configuration and potentially installing packages based on it. Test Plan -- New server tests
This commit is contained in:
1 parent
33e920cd16
commit
6cc0678f70
13 files changed
+119
-21
No files matched your search
Generated
+1
@@ -3763,6 +3763,7 @@ dependencies = [
|
||||
"shellexpand",
|
||||
"smallvec",
|
||||
"tempfile",
|
||||
"test-case",
|
||||
"thiserror 2.0.20",
|
||||
"toml 1.1.4+spec-1.1.0",
|
||||
"tracing",
|
||||
|
||||
@@ -14,6 +14,7 @@ when deciding which inputs are trusted. Browser integrations have a separate
|
||||
- **Trusted local input:** the editor, its extensions and their bundled files, the local machine and
|
||||
its file system, and the user's configuration and trust decisions.
|
||||
|
||||
Ruff treats workspaces as trusted unless the server starts with `--untrusted-workspace`.
|
||||
ty treats workspaces as trusted unless `untrustedWorkspace` is true at initialization. Trust extends
|
||||
to everything in the workspace, including source code, configuration, notebooks, and the targets of
|
||||
symlinks that point outside it.
|
||||
|
||||
@@ -627,6 +627,10 @@ pub struct FormatCommand {
|
||||
|
||||
#[derive(Copy, Clone, Debug, clap::Parser)]
|
||||
pub struct ServerCommand {
|
||||
/// Treat all workspaces as untrusted, disabling the uv formatter backend.
|
||||
#[arg(long)]
|
||||
untrusted_workspace: bool,
|
||||
|
||||
/// Enable preview mode. Use `--no-preview` to disable.
|
||||
///
|
||||
/// This enables unstable server features and turns on the preview mode for the linter
|
||||
@@ -638,6 +642,14 @@ pub struct ServerCommand {
|
||||
}
|
||||
|
||||
impl ServerCommand {
|
||||
pub(crate) fn resolve_workspace_trust(self) -> ruff_server::WorkspaceTrust {
|
||||
if self.untrusted_workspace {
|
||||
ruff_server::WorkspaceTrust::Untrusted
|
||||
} else {
|
||||
ruff_server::WorkspaceTrust::Trusted
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_preview(self) -> Option<bool> {
|
||||
resolve_bool_arg(self.preview, self.no_preview)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
use crate::ExitStatus;
|
||||
use anyhow::Result;
|
||||
use ruff_server::WorkspaceTrust;
|
||||
|
||||
pub(crate) fn run_server(preview: Option<bool>) -> Result<ExitStatus> {
|
||||
ruff_server::run(preview)?;
|
||||
pub(crate) fn run_server(
|
||||
preview: Option<bool>,
|
||||
workspace_trust: WorkspaceTrust,
|
||||
) -> Result<ExitStatus> {
|
||||
ruff_server::run(preview, workspace_trust)?;
|
||||
Ok(ExitStatus::Success)
|
||||
}
|
||||
@@ -231,7 +231,7 @@ fn analyze_graph(
|
||||
}
|
||||
|
||||
fn server(args: ServerCommand) -> Result<ExitStatus> {
|
||||
commands::server::run_server(args.resolve_preview())
|
||||
commands::server::run_server(args.resolve_preview(), args.resolve_workspace_trust())
|
||||
}
|
||||
|
||||
pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result<ExitStatus> {
|
||||
|
||||
@@ -54,6 +54,7 @@ dunce = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
smallvec = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
test-case = { workspace = true }
|
||||
|
||||
[features]
|
||||
test-uv = []
|
||||
|
||||
@@ -36,11 +36,18 @@ const NOTEBOOK_SOURCE_ORGANIZE_IMPORTS_RUFF: CodeActionKind =
|
||||
/// result type is needed.
|
||||
pub(crate) type Result<T> = anyhow::Result<T>;
|
||||
|
||||
/// Whether the server may execute code from the workspace.
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
pub enum WorkspaceTrust {
|
||||
Trusted,
|
||||
Untrusted,
|
||||
}
|
||||
|
||||
fn version() -> &'static str {
|
||||
ruff_linter::VERSION
|
||||
}
|
||||
|
||||
pub fn run(preview: Option<bool>) -> Result<()> {
|
||||
pub fn run(preview: Option<bool>, workspace_trust: WorkspaceTrust) -> Result<()> {
|
||||
let four = NonZeroUsize::new(4).unwrap();
|
||||
|
||||
// by default, we set the number of worker threads to `num_cpus`, with a maximum of 4.
|
||||
@@ -50,7 +57,7 @@ pub fn run(preview: Option<bool>) -> Result<()> {
|
||||
|
||||
let (connection, io_threads) = ConnectionInitializer::stdio();
|
||||
|
||||
let server_result = Server::new(worker_threads, connection, preview, false)
|
||||
let server_result = Server::new(worker_threads, connection, preview, workspace_trust, false)
|
||||
.context("Failed to start server")?
|
||||
.run();
|
||||
|
||||
|
||||
@@ -24,11 +24,11 @@ use types::WorkspaceFoldersServerCapabilities;
|
||||
pub use self::connection::ConnectionInitializer;
|
||||
pub(crate) use self::connection::ConnectionSender;
|
||||
use self::schedule::spawn_main_loop;
|
||||
use crate::PositionEncoding;
|
||||
pub(crate) use crate::server::main_loop::MainLoopSender;
|
||||
pub(crate) use crate::server::main_loop::{Event, MainLoopReceiver};
|
||||
use crate::session::{AllOptions, Client, Session};
|
||||
use crate::workspace::Workspaces;
|
||||
use crate::{PositionEncoding, WorkspaceTrust};
|
||||
pub(crate) use api::Error;
|
||||
|
||||
mod api;
|
||||
@@ -52,6 +52,7 @@ impl Server {
|
||||
worker_threads: NonZeroUsize,
|
||||
connection: ConnectionInitializer,
|
||||
preview: Option<bool>,
|
||||
workspace_trust: WorkspaceTrust,
|
||||
is_test: bool,
|
||||
) -> crate::Result<Self> {
|
||||
let (id, init_params) = connection.initialize_start()?;
|
||||
@@ -121,6 +122,7 @@ impl Server {
|
||||
global,
|
||||
&workspaces,
|
||||
&client,
|
||||
workspace_trust,
|
||||
)?,
|
||||
client_capabilities,
|
||||
})
|
||||
|
||||
@@ -45,10 +45,7 @@ impl super::BackgroundDocumentRequestHandler for Format {
|
||||
pub(super) fn format_full_document(snapshot: &DocumentSnapshot) -> Result<Fixes> {
|
||||
let mut fixes = Fixes::default();
|
||||
let query = snapshot.query();
|
||||
let backend = snapshot
|
||||
.client_settings()
|
||||
.editor_settings()
|
||||
.format_backend();
|
||||
let backend = snapshot.format_backend();
|
||||
|
||||
match snapshot.query() {
|
||||
DocumentQuery::Notebook { notebook, .. } => {
|
||||
@@ -84,10 +81,7 @@ fn format_document(snapshot: &DocumentSnapshot) -> Result<super::FormatResponse>
|
||||
.context("Failed to get text document for the format request")
|
||||
.unwrap();
|
||||
let query = snapshot.query();
|
||||
let backend = snapshot
|
||||
.client_settings()
|
||||
.editor_settings()
|
||||
.format_backend();
|
||||
let backend = snapshot.format_backend();
|
||||
format_text_document(
|
||||
text_document,
|
||||
query,
|
||||
|
||||
@@ -47,10 +47,7 @@ fn format_document_range(
|
||||
.context("Failed to get text document for the format range request")
|
||||
.unwrap();
|
||||
let query = snapshot.query();
|
||||
let backend = snapshot
|
||||
.client_settings()
|
||||
.editor_settings()
|
||||
.format_backend();
|
||||
let backend = snapshot.format_backend();
|
||||
format_text_document_range(text_document, range, query, snapshot.encoding(), backend)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,10 +7,11 @@ use lsp_types::{ClientCapabilities, FileEvent, NotebookDocumentCellChanges, Uri}
|
||||
use settings::ClientSettings;
|
||||
|
||||
use crate::edit::{DocumentKey, DocumentVersion, NotebookDocument};
|
||||
use crate::format::FormatBackend;
|
||||
use crate::session::request_queue::RequestQueue;
|
||||
use crate::session::settings::GlobalClientSettings;
|
||||
use crate::workspace::Workspaces;
|
||||
use crate::{PositionEncoding, TextDocument};
|
||||
use crate::{PositionEncoding, TextDocument, WorkspaceTrust};
|
||||
|
||||
pub(crate) use self::capabilities::ResolvedClientCapabilities;
|
||||
pub(crate) use self::index::DocumentQuery;
|
||||
@@ -34,6 +35,9 @@ pub(crate) struct Session {
|
||||
/// Global settings provided by the client.
|
||||
global_settings: GlobalClientSettings,
|
||||
|
||||
/// Set at startup so client settings cannot enable external formatting in an untrusted workspace.
|
||||
workspace_trust: WorkspaceTrust,
|
||||
|
||||
/// Tracks what LSP features the client supports and doesn't support.
|
||||
resolved_client_capabilities: Arc<ResolvedClientCapabilities>,
|
||||
|
||||
@@ -51,6 +55,7 @@ pub(crate) struct DocumentSnapshot {
|
||||
client_settings: Arc<settings::ClientSettings>,
|
||||
document_ref: index::DocumentQuery,
|
||||
position_encoding: PositionEncoding,
|
||||
workspace_trust: WorkspaceTrust,
|
||||
}
|
||||
|
||||
impl Session {
|
||||
@@ -60,11 +65,13 @@ impl Session {
|
||||
global: GlobalClientSettings,
|
||||
workspaces: &Workspaces,
|
||||
client: &Client,
|
||||
workspace_trust: WorkspaceTrust,
|
||||
) -> crate::Result<Self> {
|
||||
Ok(Self {
|
||||
position_encoding,
|
||||
index: index::Index::new(workspaces, &global, client)?,
|
||||
global_settings: global,
|
||||
workspace_trust,
|
||||
resolved_client_capabilities: Arc::new(ResolvedClientCapabilities::new(
|
||||
client_capabilities,
|
||||
)),
|
||||
@@ -104,6 +111,7 @@ impl Session {
|
||||
.unwrap_or_else(|| self.global_settings.to_settings_arc()),
|
||||
document_ref: self.index.make_document_ref(key, &self.global_settings)?,
|
||||
position_encoding: self.position_encoding,
|
||||
workspace_trust: self.workspace_trust,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -215,6 +223,21 @@ impl Session {
|
||||
}
|
||||
|
||||
impl DocumentSnapshot {
|
||||
pub(crate) fn format_backend(&self) -> FormatBackend {
|
||||
let backend = self.client_settings.editor_settings().format_backend();
|
||||
match self.workspace_trust {
|
||||
WorkspaceTrust::Trusted => backend,
|
||||
WorkspaceTrust::Untrusted => {
|
||||
if backend == FormatBackend::Uv {
|
||||
tracing::info!(
|
||||
"Using the internal formatter because the workspace is untrusted; the uv backend is disabled"
|
||||
);
|
||||
}
|
||||
FormatBackend::Internal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn resolved_client_capabilities(&self) -> &ResolvedClientCapabilities {
|
||||
&self.resolved_client_capabilities
|
||||
}
|
||||
@@ -242,3 +265,49 @@ impl DocumentSnapshot {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use lsp_types::Uri;
|
||||
use serde_json::json;
|
||||
use test_case::test_case;
|
||||
|
||||
use super::index::Index;
|
||||
use super::options::GlobalOptions;
|
||||
use super::{Client, Session};
|
||||
use crate::format::FormatBackend;
|
||||
use crate::session::request_queue::RequestQueue;
|
||||
use crate::{PositionEncoding, TextDocument, WorkspaceTrust};
|
||||
|
||||
#[test_case(WorkspaceTrust::Untrusted, FormatBackend::Internal)]
|
||||
#[test_case(WorkspaceTrust::Trusted, FormatBackend::Uv)]
|
||||
fn format_backend(workspace_trust: WorkspaceTrust, expected: FormatBackend) -> Result<()> {
|
||||
let (main_loop_sender, _) = crossbeam::channel::unbounded();
|
||||
let (client_sender, _) = crossbeam::channel::unbounded();
|
||||
let client = Client::new(main_loop_sender, client_sender);
|
||||
let options: GlobalOptions = serde_json::from_value(json!({
|
||||
"format": {"backend": "uv"},
|
||||
}))?;
|
||||
let mut session = Session {
|
||||
index: Index::default(),
|
||||
position_encoding: PositionEncoding::default(),
|
||||
global_settings: options.into_settings(client),
|
||||
workspace_trust,
|
||||
resolved_client_capabilities: Arc::default(),
|
||||
request_queue: RequestQueue::new(),
|
||||
shutdown_requested: false,
|
||||
};
|
||||
let uri: Uri = "untitled:test.py".parse()?;
|
||||
session.open_text_document(uri.clone(), TextDocument::new(String::new(), 1));
|
||||
let snapshot = session
|
||||
.take_snapshot(uri)
|
||||
.context("missing document snapshot")?;
|
||||
|
||||
assert_eq!(snapshot.format_backend(), expected);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -66,7 +66,7 @@ use lsp_types::{
|
||||
DidChangeWorkspaceFoldersNotification, DidCloseTextDocumentNotification,
|
||||
DidOpenTextDocumentNotification, ExitNotification, InitializedNotification, Notification,
|
||||
};
|
||||
use ruff_server::{ConnectionInitializer, LogLevel, Server, init_logging};
|
||||
use ruff_server::{ConnectionInitializer, LogLevel, Server, WorkspaceTrust, init_logging};
|
||||
use rustc_hash::FxHashMap;
|
||||
use tempfile::TempDir;
|
||||
|
||||
@@ -207,7 +207,13 @@ impl TestServer {
|
||||
// TODO: This should probably be configurable to test concurrency issues
|
||||
let worker_threads = NonZeroUsize::new(1).unwrap();
|
||||
|
||||
match Server::new(worker_threads, server_connection, None, true) {
|
||||
match Server::new(
|
||||
worker_threads,
|
||||
server_connection,
|
||||
None,
|
||||
WorkspaceTrust::Trusted,
|
||||
true,
|
||||
) {
|
||||
Ok(server) => {
|
||||
if let Err(err) = server.run() {
|
||||
panic!("Server stopped with error: {err:?}");
|
||||
|
||||
@@ -981,6 +981,10 @@ The backend to use for formatting files. Following options are available:
|
||||
For `internal`, the formatter version will match the selected Ruff version while for `uv`, the
|
||||
formatter version may differ.
|
||||
|
||||
Starting the server with `ruff server --untrusted-workspace` disables the uv backend. With this flag,
|
||||
the server always uses the internal formatter, regardless of this setting. This prevents workspace
|
||||
configuration from causing uv to execute untrusted code.
|
||||
|
||||
**Default value**: `"internal"`
|
||||
|
||||
**Type**: `"internal" | "uv"`
|
||||
|
||||
Reference in new issue
Block a user