ruff server: Support a custom TOML configuration file (#11140)

## Summary

Closes #10985.

The server now supports a custom TOML configuration file as a client
setting. The setting must be an absolute path to a file. If the file is
called `pyproject.toml`, the server will attempt to parse it as a
pyproject file - otherwise, it will attempt to parse it as a `ruff.toml`
file, even if the file has a name besides `ruff.toml`.

If an option is set in both the custom TOML configuration file and in
the client settings directly, the latter will be used.

## Test Plan

1. Create a `ruff.toml` file outside of the workspace you are testing.
Set an option that is different from the one in the configuration for
your test workspace.
2. Set the path to the configuration in NeoVim:
```lua
require('lspconfig').ruff.setup {
    init_options = {
      settings = {
        configuration = "absolute/path/to/your/configuration"
      }
    }
}
```
3. Confirm that the option in the configuration file is used, regardless
of what the option is set to in the workspace configuration.
4. Add the same option, with a different value, to the NeoVim
configuration directly. For example:
```lua
require('lspconfig').ruff.setup {
    init_options = {
      settings = {
        configuration = "absolute/path/to/your/configuration",
        lint = {
          select = []
        }
      }
    }
}
```
5. Confirm that the option set in client settings is used, regardless of
the value in either the custom configuration file or in the workspace
configuration.
This commit is contained in:
Jane Lewis 2024-04-26 16:46:07 -07:00 committed by GitHub
parent 77a72ecd38
commit 632965d0fa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 41 additions and 4 deletions

View file

@ -1,4 +1,4 @@
use std::{ops::Deref, str::FromStr};
use std::{ffi::OsString, ops::Deref, path::PathBuf, str::FromStr};
use lsp_types::Url;
use ruff_linter::{line_width::LineLength, RuleSelector};
@ -31,6 +31,7 @@ pub(crate) struct ResolvedClientSettings {
#[derive(Clone, Debug)]
#[cfg_attr(test, derive(PartialEq, Eq))]
pub(crate) struct ResolvedEditorSettings {
pub(super) configuration: Option<PathBuf>,
pub(super) lint_preview: Option<bool>,
pub(super) format_preview: Option<bool>,
pub(super) select: Option<Vec<RuleSelector>>,
@ -44,11 +45,10 @@ pub(crate) struct ResolvedEditorSettings {
/// Determines how multiple conflicting configurations should be resolved - in this
/// case, the configuration from the client settings and configuration from local
/// `.toml` files (aka 'workspace' configuration).
#[derive(Clone, Copy, Debug, Deserialize, Default)]
#[cfg_attr(test, derive(PartialEq, Eq))]
#[derive(Clone, Copy, Debug, Deserialize, Default, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub(crate) enum ConfigurationPreference {
/// Configuration set in the editor takes priority over workspace configuration set in `.toml` files.
/// Configuration set in the editor takes priority over configuration set in `.toml` files.
#[default]
EditorFirst,
/// Configuration set in `.toml` files takes priority over configuration set in the editor.
@ -62,6 +62,7 @@ pub(crate) enum ConfigurationPreference {
#[cfg_attr(test, derive(PartialEq, Eq))]
#[serde(rename_all = "camelCase")]
pub(crate) struct ClientSettings {
configuration: Option<String>,
fix_all: Option<bool>,
organize_imports: Option<bool>,
lint: Option<LintOptions>,
@ -229,6 +230,12 @@ impl ResolvedClientSettings {
true,
),
editor_settings: ResolvedEditorSettings {
configuration: Self::resolve_optional(all_settings, |settings| {
settings
.configuration
.as_ref()
.map(|config_path| OsString::from(config_path.clone()).into())
}),
lint_preview: Self::resolve_optional(all_settings, |settings| {
settings.lint.as_ref()?.preview
}),
@ -357,6 +364,7 @@ mod tests {
assert_debug_snapshot!(options, @r###"
HasWorkspaces {
global_settings: ClientSettings {
configuration: None,
fix_all: Some(
false,
),
@ -411,6 +419,7 @@ mod tests {
workspace_settings: [
WorkspaceSettings {
settings: ClientSettings {
configuration: None,
fix_all: Some(
true,
),
@ -469,6 +478,7 @@ mod tests {
},
WorkspaceSettings {
settings: ClientSettings {
configuration: None,
fix_all: Some(
true,
),
@ -555,6 +565,7 @@ mod tests {
disable_rule_comment_enable: false,
fix_violation_enable: false,
editor_settings: ResolvedEditorSettings {
configuration: None,
lint_preview: Some(true),
format_preview: None,
select: Some(vec![
@ -584,6 +595,7 @@ mod tests {
disable_rule_comment_enable: true,
fix_violation_enable: false,
editor_settings: ResolvedEditorSettings {
configuration: None,
lint_preview: Some(false),
format_preview: None,
select: Some(vec![
@ -608,6 +620,7 @@ mod tests {
GlobalOnly {
settings: Some(
ClientSettings {
configuration: None,
fix_all: Some(
false,
),
@ -671,6 +684,7 @@ mod tests {
disable_rule_comment_enable: false,
fix_violation_enable: true,
editor_settings: ResolvedEditorSettings {
configuration: None,
lint_preview: None,
format_preview: None,
select: None,

View file

@ -110,6 +110,7 @@ impl<'a> ConfigurationTransformer for EditorConfigurationTransformer<'a> {
filesystem_configuration: ruff_workspace::configuration::Configuration,
) -> ruff_workspace::configuration::Configuration {
let ResolvedEditorSettings {
configuration,
format_preview,
lint_preview,
select,
@ -150,6 +151,19 @@ impl<'a> ConfigurationTransformer for EditorConfigurationTransformer<'a> {
..Default::default()
};
// Merge in the editor-specified configuration file, if it exists
let editor_configuration = if let Some(config_file_path) = configuration {
match open_configuration_file(&config_file_path, project_root) {
Ok(config_from_file) => editor_configuration.combine(config_from_file),
Err(err) => {
tracing::error!("Unable to find editor-specified configuration file {err}");
editor_configuration
}
}
} else {
editor_configuration
};
match configuration_preference {
ConfigurationPreference::EditorFirst => {
editor_configuration.combine(filesystem_configuration)
@ -161,3 +175,12 @@ impl<'a> ConfigurationTransformer for EditorConfigurationTransformer<'a> {
}
}
}
fn open_configuration_file(
config_path: &Path,
project_root: &Path,
) -> crate::Result<Configuration> {
let options = ruff_workspace::pyproject::load_options(config_path)?;
Configuration::from_options(options, Some(config_path), project_root)
}