[ty] Preserve valid LSP settings when configuration resolution fails (#28653)

This commit is contained in:
Micha Reiser authored and GitHub committed 2026-09-17 09:48:31 +02:00
1 parent aeb51db2b8
commit 72b1fd3cef
3 files changed
+162 -37

No files matched your search

+37 -36
View File
@@ -777,47 +777,48 @@ impl Session {
ProjectMetadata::discover_with_uv(workspace_directory, &system, self.use_uv)
};
let project = metadata
.context("Failed to discover project configuration")
.and_then(|mut metadata| {
if let Some(fallback_options) = workspace.settings.fallback_options() {
metadata.set_fallback_options(fallback_options.clone());
let (mut metadata, discovery_result) =
match metadata.context("Failed to discover project configuration") {
Ok(metadata) => (metadata, Ok(())),
Err(err) => {
let Ok(metadata) = ProjectMetadata::from_options(
Options::default(),
workspace_directory.to_path_buf(),
None,
&UseDefaultStrategy,
);
(metadata.with_use_uv(self.use_uv), Err(err))
}
};
if let Some(fallback_options) = workspace.settings.fallback_options() {
metadata.set_fallback_options(fallback_options.clone());
}
if let Some(override_options) = workspace.settings.override_options() {
metadata.set_override_options(override_options.clone());
}
// Load user configuration even if project discovery failed.
let project = discovery_result
.and(
metadata
.apply_configuration_files(&system)
.context("Failed to apply configuration files")?;
.context("Failed to apply configuration files"),
)
.and_then(|()| ProjectDatabase::fallible(metadata.clone(), system.clone()));
if let Some(override_options) = workspace.settings.override_options() {
metadata.set_override_options(override_options.clone());
}
ProjectDatabase::fallible(metadata, system.clone())
});
let mut db = match project {
Ok(db) => db,
Err(err) => {
tracing::error!(
"Failed to create project for workspace `{uri}`: {err:#}. \
Falling back to default settings"
);
client.show_error_message(format!(
"Failed to load project for workspace {uri}. {}",
self.client_name.log_guidance(),
));
let Ok(metadata) = ProjectMetadata::from_options(
Options::default(),
workspace_directory.to_path_buf(),
None,
&UseDefaultStrategy,
)
.map(|metadata| metadata.with_use_uv(self.use_uv));
ProjectDatabase::use_defaults(metadata, system)
}
};
let mut db = project.unwrap_or_else(|err| {
tracing::error!(
"Failed to load project for workspace `{uri}`: {err:#}. \
Continuing with valid settings"
);
client.show_error_message(format!(
"Failed to load project for workspace {uri}. {}",
self.client_name.log_guidance(),
));
ProjectDatabase::use_defaults(metadata, system)
});
// Carry forward diagnostic state if any exists
let previous = self.projects.remove(&root);
+124
View File
@@ -1,11 +1,13 @@
use anyhow::Result;
use insta::assert_json_snapshot;
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<()> {
@@ -73,6 +75,16 @@ def foo() -> str:
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()
@@ -104,6 +116,118 @@ unresolved-reference="warn"
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();
@@ -16,7 +16,7 @@ expression: diagnostics
"character": 12
}
},
"severity": 1,
"severity": 2,
"code": "unresolved-reference",
"codeDescription": {
"href": "https://ty.dev/rules#unresolved-reference"