mirror of
https://github.com/astral-sh/ruff.git
synced 2026-10-28 10:45:37 +08:00
## Summary Same as https://github.com/astral-sh/ruff/pull/28125 but for the server. The main difference to the CLI is that, up to now, the server did not support changing its watched paths after its initial initialization. This PR adds the infrastructure so that the server now supports updating its watched paths as search paths change. ## Test Plan **Before** https://github.com/user-attachments/assets/a6ace5a1-84c9-4acb-a15c-991389543109 Changes to `dependency.py` are not picked up **Now** https://github.com/user-attachments/assets/b3d6bf4b-496f-4bdc-a610-82eb9690ffb3
571 lines
16 KiB
Rust
571 lines
16 KiB
Rust
use anyhow::{Context, Result};
|
|
use insta::assert_json_snapshot;
|
|
use lsp_types::RegistrationRequest;
|
|
use ruff_db::system::SystemPath;
|
|
use ruff_python_trivia::textwrap::dedent;
|
|
use serde_json::{Map, json};
|
|
use ty_server::{ClientOptions, WorkspaceOptions};
|
|
|
|
use crate::TestServerBuilder;
|
|
use crate::pull_diagnostics::filter_result_id;
|
|
use crate::workspace_folders::condensed_document_diagnostic_snapshot;
|
|
|
|
#[test]
|
|
fn configuration_file() -> Result<()> {
|
|
let _filter = filter_result_id();
|
|
|
|
let workspace_root = SystemPath::new("src");
|
|
let foo = SystemPath::new("src/foo.py");
|
|
let foo_content = "\
|
|
def foo() -> str:
|
|
return a
|
|
";
|
|
|
|
let builder = TestServerBuilder::new()?;
|
|
|
|
let settings_path = builder.file_path("ty[dev].toml");
|
|
|
|
let mut server = builder
|
|
.with_workspace(
|
|
workspace_root,
|
|
Some(ClientOptions {
|
|
workspace: WorkspaceOptions {
|
|
configuration_file: Some(settings_path.to_string()),
|
|
..WorkspaceOptions::default()
|
|
},
|
|
..ClientOptions::default()
|
|
}),
|
|
)?
|
|
.with_file(foo, foo_content)?
|
|
.with_file(
|
|
settings_path,
|
|
r#"
|
|
[rules]
|
|
unresolved-reference="warn"
|
|
"#,
|
|
)?
|
|
.with_watched_file_support(true)
|
|
.build()
|
|
.wait_until_workspaces_are_initialized();
|
|
|
|
let (_, registrations) = server.await_request::<RegistrationRequest>();
|
|
let options = registrations
|
|
.registrations
|
|
.first()
|
|
.and_then(|registration| registration.register_options.as_ref())
|
|
.context("expected file watcher registration")?;
|
|
assert_json_snapshot!(options["watchers"], @r#"
|
|
[
|
|
{
|
|
"globPattern": {
|
|
"baseUri": "file://<temp_dir>/src",
|
|
"pattern": "**"
|
|
}
|
|
},
|
|
{
|
|
"globPattern": {
|
|
"baseUri": "file://<temp_dir>/",
|
|
"pattern": "ty[[]dev[]].toml"
|
|
}
|
|
}
|
|
]
|
|
"#);
|
|
|
|
server.open_text_document(foo, foo_content, 1);
|
|
let diagnostics = server.document_diagnostic_request(foo, None);
|
|
|
|
assert_json_snapshot!("configuration_file", diagnostics);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_configuration_file() -> Result<()> {
|
|
let _filter = filter_result_id();
|
|
|
|
let workspace_root = SystemPath::new("src");
|
|
let foo = SystemPath::new("src/foo.py");
|
|
let foo_content = "\
|
|
def foo() -> str:
|
|
return a
|
|
";
|
|
|
|
let builder = TestServerBuilder::new()?;
|
|
|
|
let settings_path = builder.file_path("ty2.toml");
|
|
|
|
let mut server = builder
|
|
.with_workspace(
|
|
workspace_root,
|
|
Some(ClientOptions {
|
|
workspace: WorkspaceOptions {
|
|
configuration_file: Some(settings_path.to_string()),
|
|
configuration: Some(
|
|
Map::from_iter([
|
|
("rules".to_string(), json!({"unresolved-reference": "warn"})),
|
|
(
|
|
"environment".to_string(),
|
|
json!({"python": "./missing-environment"}),
|
|
),
|
|
])
|
|
.into(),
|
|
),
|
|
..WorkspaceOptions::default()
|
|
},
|
|
..ClientOptions::default()
|
|
}),
|
|
)?
|
|
.with_file(foo, foo_content)?
|
|
.with_file(
|
|
settings_path,
|
|
r#"
|
|
[rule]
|
|
unresolved-reference="warn"
|
|
"#,
|
|
)?
|
|
.build()
|
|
.wait_until_workspaces_are_initialized();
|
|
|
|
server.open_text_document(foo, foo_content, 1);
|
|
let show_message = server.await_notification::<lsp_types::ShowMessageNotification>();
|
|
let diagnostics = server.document_diagnostic_request(foo, None);
|
|
|
|
assert_json_snapshot!(show_message, @r#"
|
|
{
|
|
"type": 1,
|
|
"message": "Failed to load project for workspace file://<temp_dir>/src. Please refer to the logs for more details."
|
|
}
|
|
"#);
|
|
assert_json_snapshot!(diagnostics);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_python_environment_preserves_valid_configuration() -> Result<()> {
|
|
let main = SystemPath::new("src/main.py");
|
|
let content = dedent(
|
|
"
|
|
import sys
|
|
sys.platform
|
|
",
|
|
);
|
|
|
|
let mut server = TestServerBuilder::new()?
|
|
.with_workspace(SystemPath::new("src"), None)?
|
|
.with_file(
|
|
"src/ty.toml",
|
|
dedent(
|
|
r#"
|
|
[environment]
|
|
python = "./missing-environment"
|
|
python-platform = "win32"
|
|
"#,
|
|
),
|
|
)?
|
|
.with_file(main, &content)?
|
|
.build()
|
|
.wait_until_workspaces_are_initialized();
|
|
|
|
let message = server.await_notification::<lsp_types::ShowMessageNotification>();
|
|
assert_eq!(message.kind, lsp_types::MessageType::Error);
|
|
|
|
server.open_text_document(main, &content, 1);
|
|
let platform = server.hover_request(main, lsp_types::Position::new(2, 5));
|
|
assert_json_snapshot!(platform, @r#"
|
|
{
|
|
"contents": {
|
|
"kind": "plaintext",
|
|
"value": "Literal[\"win32\"]"
|
|
},
|
|
"range": {
|
|
"start": {
|
|
"line": 2,
|
|
"character": 4
|
|
},
|
|
"end": {
|
|
"line": 2,
|
|
"character": 12
|
|
}
|
|
}
|
|
}
|
|
"#);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_include_pattern_preserves_rules_and_diagnostics() -> Result<()> {
|
|
let main = SystemPath::new("src/main.py");
|
|
let content = "undefined";
|
|
let mut server = TestServerBuilder::new()?
|
|
.with_workspace(SystemPath::new("src"), None)?
|
|
.with_file(
|
|
"src/ty.toml",
|
|
dedent(
|
|
r#"
|
|
[src]
|
|
include = ["["]
|
|
|
|
[rules]
|
|
unresolved-reference = "warn"
|
|
unknown-rule = "warn"
|
|
"#,
|
|
),
|
|
)?
|
|
.with_file(main, content)?
|
|
.build()
|
|
.wait_until_workspaces_are_initialized();
|
|
|
|
let message = server.await_notification::<lsp_types::ShowMessageNotification>();
|
|
assert_eq!(message.kind, lsp_types::MessageType::Error);
|
|
|
|
let settings_diagnostics =
|
|
server.await_notification::<lsp_types::PublishDiagnosticsNotification>();
|
|
assert_json_snapshot!(settings_diagnostics, @r#"
|
|
{
|
|
"uri": "file://<temp_dir>/src/ty.toml",
|
|
"diagnostics": [
|
|
{
|
|
"range": {
|
|
"start": {
|
|
"line": 6,
|
|
"character": 0
|
|
},
|
|
"end": {
|
|
"line": 6,
|
|
"character": 12
|
|
}
|
|
},
|
|
"severity": 2,
|
|
"code": "unknown-rule",
|
|
"source": "ty",
|
|
"message": "Unknown rule `unknown-rule`. Did you mean `unknown-argument`?"
|
|
}
|
|
]
|
|
}
|
|
"#);
|
|
|
|
server.open_text_document(main, content, 1);
|
|
let diagnostics = server.document_diagnostic_request(main, None);
|
|
insta::assert_snapshot!(condensed_document_diagnostic_snapshot(diagnostics), @"0:0..0:9[WARNING]: Name `undefined` used when not defined");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn configuration_overrides() -> Result<()> {
|
|
let _filter = filter_result_id();
|
|
|
|
let workspace_root = SystemPath::new("src");
|
|
let foo = SystemPath::new("src/foo.py");
|
|
let foo_content = "\
|
|
def foo() -> str:
|
|
return a
|
|
";
|
|
|
|
let mut server = TestServerBuilder::new()?
|
|
.with_workspace(
|
|
workspace_root,
|
|
Some(ClientOptions {
|
|
workspace: WorkspaceOptions {
|
|
configuration: Some(
|
|
Map::from_iter([(
|
|
"rules".to_string(),
|
|
json!({"unresolved-reference": "warn"}),
|
|
)])
|
|
.into(),
|
|
),
|
|
..WorkspaceOptions::default()
|
|
},
|
|
..ClientOptions::default()
|
|
}),
|
|
)?
|
|
.with_file(foo, foo_content)?
|
|
.build()
|
|
.wait_until_workspaces_are_initialized();
|
|
|
|
server.open_text_document(foo, foo_content, 1);
|
|
let diagnostics = server.document_diagnostic_request(foo, None);
|
|
|
|
assert_json_snapshot!(diagnostics);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn unsupported_editor_python_version() -> Result<()> {
|
|
let _filter = filter_result_id();
|
|
|
|
let workspace_root = SystemPath::new("src");
|
|
let main = SystemPath::new("src/main.py");
|
|
let python_home = "base/bin";
|
|
let base_python = if cfg!(target_os = "windows") {
|
|
"base/bin/python.exe"
|
|
} else {
|
|
"base/bin/python"
|
|
};
|
|
let python = if cfg!(target_os = "windows") {
|
|
"venv/Scripts/python.exe"
|
|
} else {
|
|
"venv/bin/python"
|
|
};
|
|
let site_packages_foo = if cfg!(target_os = "windows") {
|
|
"venv/Lib/site-packages/foo.py"
|
|
} else {
|
|
"venv/lib/python3.16/site-packages/foo.py"
|
|
};
|
|
// The import proves we still use the editor-selected environment for module resolution even
|
|
// when we ignore its unsupported reported Python version.
|
|
let foo_content = "\
|
|
import foo
|
|
import sys
|
|
from typing_extensions import reveal_type
|
|
|
|
reveal_type(sys.version_info[:2])
|
|
";
|
|
|
|
let builder = TestServerBuilder::new()?;
|
|
let python_home = builder.file_path(python_home);
|
|
let sys_prefix = builder.file_path("venv");
|
|
|
|
let workspace_options: ClientOptions = serde_json::from_value(json!({
|
|
"pythonExtension": {
|
|
"activeEnvironment": {
|
|
"executable": {
|
|
"sysPrefix": sys_prefix,
|
|
},
|
|
"version": {
|
|
"major": 3,
|
|
"minor": 16,
|
|
}
|
|
}
|
|
}
|
|
}))?;
|
|
|
|
let mut server = builder
|
|
.with_workspace(workspace_root, Some(workspace_options))?
|
|
.with_file(main, foo_content)?
|
|
.with_file(base_python, "")?
|
|
.with_file(python, "")?
|
|
.with_file(
|
|
"venv/pyvenv.cfg",
|
|
format!("version_info = 3.16.0\nhome = {python_home}\n"),
|
|
)?
|
|
.with_file(site_packages_foo, "")?
|
|
.build()
|
|
.wait_until_workspaces_are_initialized();
|
|
|
|
// The unsupported version inferred from the selected environment surfaces as a
|
|
// settings diagnostic on the environment's `pyvenv.cfg`.
|
|
let diagnostics = server.collect_publish_diagnostic_notifications(1);
|
|
assert_json_snapshot!(diagnostics, @r#"
|
|
{
|
|
"file://<temp_dir>/venv/pyvenv.cfg": [
|
|
{
|
|
"range": {
|
|
"start": {
|
|
"line": 0,
|
|
"character": 15
|
|
},
|
|
"end": {
|
|
"line": 0,
|
|
"character": 21
|
|
}
|
|
},
|
|
"severity": 2,
|
|
"code": "unsupported-python-version",
|
|
"source": "ty",
|
|
"message": "Ignoring unsupported inferred Python version `3.16`; ty will use Python 3.14 instead.\n\ninfo: Expected one of `3.7`, `3.8`, `3.9`, `3.10`, `3.11`, `3.12`, `3.13`, `3.14`, `3.15`.\ninfo: Set `environment.python-version` explicitly to override the inferred version.\ninfo: The version was inferred from your virtual environment metadata."
|
|
}
|
|
]
|
|
}
|
|
"#);
|
|
|
|
server.open_text_document(main, foo_content, 1);
|
|
let diagnostics = server.document_diagnostic_request(main, None);
|
|
|
|
assert_json_snapshot!(diagnostics, @r#"
|
|
{
|
|
"resultId": "[RESULT_ID]",
|
|
"items": [
|
|
{
|
|
"range": {
|
|
"start": {
|
|
"line": 4,
|
|
"character": 12
|
|
},
|
|
"end": {
|
|
"line": 4,
|
|
"character": 32
|
|
}
|
|
},
|
|
"severity": 3,
|
|
"code": "revealed-type",
|
|
"source": "ty",
|
|
"message": "Revealed type: `tuple[Literal[3], Literal[14]]`"
|
|
}
|
|
],
|
|
"kind": "full"
|
|
}
|
|
"#);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg_attr(windows, ignore = "site-packages layout inference is Unix-only")]
|
|
#[test]
|
|
fn unsupported_inferred_python_version_setting_diagnostic() -> Result<()> {
|
|
let workspace_root = SystemPath::new("project");
|
|
let main = SystemPath::new("project/main.py");
|
|
let python_home = "base/bin";
|
|
let base_python = if cfg!(target_os = "windows") {
|
|
"base/bin/python.exe"
|
|
} else {
|
|
"base/bin/python"
|
|
};
|
|
let python = if cfg!(target_os = "windows") {
|
|
"project/.venv/Scripts/python.exe"
|
|
} else {
|
|
"project/.venv/bin/python"
|
|
};
|
|
let site_packages = if cfg!(target_os = "windows") {
|
|
"project/.venv/Lib/site-packages/foo.py"
|
|
} else {
|
|
"project/.venv/lib/python3.16/site-packages/foo.py"
|
|
};
|
|
|
|
let builder = TestServerBuilder::new()?;
|
|
let python_home = builder.file_path(python_home);
|
|
|
|
let mut server = builder
|
|
.with_workspace(workspace_root, None)?
|
|
.with_file(main, "x = 1\n")?
|
|
.with_file(base_python, "")?
|
|
.with_file(python, "")?
|
|
.with_file(
|
|
"project/.venv/pyvenv.cfg",
|
|
format!("home = {python_home}\n"),
|
|
)?
|
|
.with_file(site_packages, "")?
|
|
.build()
|
|
.wait_until_workspaces_are_initialized();
|
|
|
|
let diagnostics = server.collect_publish_diagnostic_notifications(1);
|
|
|
|
assert_json_snapshot!(diagnostics);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg_attr(windows, ignore = "site-packages layout inference is Unix-only")]
|
|
#[test]
|
|
fn unsupported_inferred_python_version_setting_diagnostic_for_system_interpreter() -> Result<()> {
|
|
let workspace_root = SystemPath::new("project");
|
|
let main = SystemPath::new("project/main.py");
|
|
let python = "python/bin/python3.16";
|
|
let site_packages = "python/lib/python3.16/site-packages/foo.py";
|
|
|
|
let builder = TestServerBuilder::new()?;
|
|
let sys_prefix = builder.file_path("python");
|
|
let python_uri = builder.file_uri(python);
|
|
|
|
let workspace_options: ClientOptions = serde_json::from_value(json!({
|
|
"pythonExtension": {
|
|
"activeEnvironment": {
|
|
"executable": {
|
|
"uri": python_uri,
|
|
"sysPrefix": sys_prefix,
|
|
}
|
|
}
|
|
}
|
|
}))?;
|
|
|
|
let mut server = builder
|
|
.with_workspace(workspace_root, Some(workspace_options))?
|
|
.with_file(main, "x = 1\n")?
|
|
.with_file(python, "")?
|
|
.with_file(site_packages, "")?
|
|
.build()
|
|
.wait_until_workspaces_are_initialized();
|
|
|
|
let diagnostics = server.collect_publish_diagnostic_notifications(1);
|
|
|
|
assert_json_snapshot!(diagnostics, @r#"
|
|
{
|
|
"file://<temp_dir>/python/bin/python3.16": [
|
|
{
|
|
"range": {
|
|
"start": {
|
|
"line": 0,
|
|
"character": 0
|
|
},
|
|
"end": {
|
|
"line": 0,
|
|
"character": 0
|
|
}
|
|
},
|
|
"severity": 2,
|
|
"code": "unsupported-python-version",
|
|
"source": "ty",
|
|
"message": "Ignoring unsupported inferred Python version `3.16`; ty will use Python 3.14 instead.\n\ninfo: Expected one of `3.7`, `3.8`, `3.9`, `3.10`, `3.11`, `3.12`, `3.13`, `3.14`, `3.15`.\ninfo: Set `environment.python-version` explicitly to override the inferred version.\ninfo: The version was inferred from the `lib/python3.16/site-packages` directory layout."
|
|
}
|
|
]
|
|
}
|
|
"#);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn configuration_file_and_overrides() -> Result<()> {
|
|
let _filter = filter_result_id();
|
|
|
|
let workspace_root = SystemPath::new("src");
|
|
let foo = SystemPath::new("src/foo.py");
|
|
let foo_content = "\
|
|
def foo() -> str:
|
|
return a
|
|
";
|
|
|
|
let builder = TestServerBuilder::new()?;
|
|
|
|
let settings_path = builder.file_path("ty2.toml");
|
|
|
|
let mut server = builder
|
|
.with_workspace(
|
|
workspace_root,
|
|
Some(ClientOptions {
|
|
workspace: WorkspaceOptions {
|
|
configuration_file: Some(settings_path.to_string()),
|
|
configuration: Some(
|
|
Map::from_iter([(
|
|
"rules".to_string(),
|
|
json!({"unresolved-reference": "ignore"}),
|
|
)])
|
|
.into(),
|
|
),
|
|
..WorkspaceOptions::default()
|
|
},
|
|
..ClientOptions::default()
|
|
}),
|
|
)?
|
|
.with_file(foo, foo_content)?
|
|
.with_file(
|
|
settings_path,
|
|
r#"
|
|
[rules]
|
|
unresolved-reference="warn"
|
|
"#,
|
|
)?
|
|
.build()
|
|
.wait_until_workspaces_are_initialized();
|
|
|
|
server.open_text_document(foo, foo_content, 1);
|
|
let diagnostics = server.document_diagnostic_request(foo, None);
|
|
|
|
assert_json_snapshot!(diagnostics);
|
|
|
|
Ok(())
|
|
}
|