Skip to content

Avoid running uv format in untrusted workspaces - #28873

Merged
ntBre merged 4 commits into
mainfrom
brent/ruff-untrusted-workspace
Sep 28, 2026
Merged

ntBre merged 4 commits into
mainfrom
brent/ruff-untrusted-workspace

Conversation

@ntBre

@ntBre ntBre commented Sep 24, 2026

Copy link
Copy Markdown
Contributor

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

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
@ntBre ntBre added the server Related to the Ruff or ty LSP server label Sep 24, 2026
@astral-sh-bot

astral-sh-bot Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

Typing conformance results

No changes detected ✅

Current numbers
The percentage of diagnostics emitted that were expected errors held steady at 98.24%. The percentage of expected errors that received a diagnostic held steady at 98.24%. The number of fully passing files held steady at 134/146.

@astral-sh-bot

astral-sh-bot Bot commented Sep 24, 2026

Copy link
Copy Markdown

Memory usage report

Memory usage unchanged ✅

@astral-sh-bot

astral-sh-bot Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

ecosystem-analyzer results

No diagnostic changes detected ✅

Full report with detailed diff (timing results)

@astral-sh-bot

astral-sh-bot Bot commented Sep 24, 2026

Copy link
Copy Markdown

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

✅ ecosystem check detected no linter changes.

Formatter (stable)

✅ ecosystem check detected no format changes.

Formatter (preview)

✅ ecosystem check detected no format changes.

Comment on lines +210 to +216
match Server::new(
worker_threads,
server_connection,
None,
WorkspaceTrust::Trusted,
true,
) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add a TestServer::untrusted factory method with an internal, shared new_impl? Or are there other reasons why it can't be a e2e test?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding is that the main issue is determining what to assert at the end of the test, not setting up the test server initially. There's no user-visible output that we're using one backend or the other (except the new logging call). So it makes it hard to assert something useful in the e2e tests. But you'd know better about our e2e test capabilities than me, I could certainly be missing something.

Codex started producing something like this when I asked for an e2e test, which is what I thought looked too janky to include:

fn without_uv(body: impl FnOnce() -> Result<()>) -> Result<()> {
    const CHILD: &str = "RUFF_TEST_ISOLATED_CHILD";
    let thread = std::thread::current();
    let name = thread.name().context("missing test name")?;
    if std::env::var(CHILD).as_deref() == Ok(name) {
        return body();
    }
    let empty_path = tempfile::tempdir()?;
    let output = Command::new(std::env::current_exe()?)
        .args(["--exact", name, "--nocapture"])
        .env(CHILD, name)
        .env("PATH", empty_path.path())
        .current_dir(empty_path.path())
        .output()?;
    ensure!(output.status.success(), "{}\n{}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr));
    println!("{}", String::from_utf8_lossy(&output.stdout));
    Ok(())
}

plus another ~50 lines of helpers.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can of course assert that the code gets formatted somehow, which Codex also tried initially, but that didn't seem very useful, as both backends format code.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you override path and have uv format fail?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's what Codex's snippet above was trying to do. I'll just push its patch so you can see what you think. Maybe it's not as bad as I thought.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I see, ty's server tests have more indirection here, through TestServerBuilder::with_env_var and the system stuff. Here, we end up calling straight into Command::new("uv"), which makes it much harder to inject environment variables at the right point, or without modifying other tests, which is what Codex is trying to avoid.

Anyway, I've included Codex's e2e test scheme. We could also do something more like ty, although it feels like that probably belongs in a separate PR.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh, I forgot that we're in Ruff's server where we don't have the ability to easily override commands. Hmm, I don't see a great solution other than plumbing through the argument somehow (e.g. by storing a CommandExecutor on Server). I'm not sure this is worth it for this change, or, at least, might be better as a follow up. Thanks for giving it a try

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No problem! I'll revert this last commit and merge then :)

@MichaReiser MichaReiser left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry. It looks like I forgot to submit my feedback on Friday.

Comment on lines +374 to +399
/// Isolate PATH from other tests, which run their servers in the same process.
/// Without uv, formatting only succeeds if the internal backend is used.
fn without_uv(body: impl FnOnce() -> Result<()>) -> Result<()> {
const CHILD: &str = "RUFF_TEST_ISOLATED_CHILD";
let thread = std::thread::current();
let name = thread.name().context("missing test name")?;
if std::env::var(CHILD).as_deref() == Ok(name) {
return body();
}

let empty_path = tempfile::tempdir()?;
let output = Command::new(std::env::current_exe()?)
.args(["--exact", name, "--nocapture"])
.env(CHILD, name)
.env("PATH", empty_path.path())
.current_dir(empty_path.path())
.output()?;
let stdout = String::from_utf8_lossy(&output.stdout);
// An unmatched `--exact` filter exits successfully without running any tests.
ensure!(
output.status.success() && stdout.contains("test result: ok. 1 passed;"),
"{stdout}\n{}",
String::from_utf8_lossy(&output.stderr),
);
Ok(())
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels a bit crazy to me. Seems a bit annoying to debug and keep compatibility.

Comment on lines +346 to +369
match workspace_trust {
WorkspaceTrust::Trusted => {
assert_matches!(
document,
Err(AwaitResponseError::RequestFailed(error))
if error.message.contains("uv was not found"),
);
assert_matches!(
range,
Err(AwaitResponseError::RequestFailed(error))
if error.message.contains("uv was not found"),
);
server.await_notification::<ShowMessageNotification>();
server.await_notification::<ShowMessageNotification>();
}
WorkspaceTrust::Untrusted => {
let expected = Some(vec![TextEdit {
range: Range::new(Position::new(0, 0), Position::new(1, 0)),
new_text: "x = 1\n".to_string(),
}]);
assert_eq!(document?, expected);
assert_eq!(range?, expected);
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'd prefer a helper function that sets up the test case and then have two separate tests over test_case + a match here

Comment on lines +210 to +216
match Server::new(
worker_threads,
server_connection,
None,
WorkspaceTrust::Trusted,
true,
) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh, I forgot that we're in Ruff's server where we don't have the ability to easily override commands. Hmm, I don't see a great solution other than plumbing through the argument somehow (e.g. by storing a CommandExecutor on Server). I'm not sure this is worth it for this change, or, at least, might be better as a follow up. Thanks for giving it a try

This reverts commit f18ceda.
@codspeed

codspeed Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 222 untouched benchmarks


Comparing brent/ruff-untrusted-workspace (bf78f4d) with main (33e920c)

Open in CodSpeed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

server Related to the Ruff or ty LSP server

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants