Compare commits

...

1 Commits

Author SHA1 Message Date
Dhruv Manilawala
c705787bb3 WIP 2025-02-22 10:59:20 +05:30
8 changed files with 296 additions and 44 deletions

2
Cargo.lock generated
View File

@@ -2822,6 +2822,7 @@ dependencies = [
"ruff_python_formatter",
"ruff_python_parser",
"ruff_python_trivia",
"ruff_server",
"ruff_workspace",
"schemars",
"serde",
@@ -3172,6 +3173,7 @@ dependencies = [
"ruff_diagnostics",
"ruff_formatter",
"ruff_linter",
"ruff_macros",
"ruff_notebook",
"ruff_python_ast",
"ruff_python_codegen",

View File

@@ -22,6 +22,7 @@ ruff_python_codegen = { workspace = true }
ruff_python_formatter = { workspace = true }
ruff_python_parser = { workspace = true }
ruff_python_trivia = { workspace = true }
ruff_server = { workspace = true }
ruff_workspace = { workspace = true, features = ["schemars"] }
anyhow = { workspace = true }

View File

@@ -2,6 +2,7 @@
//!
//! Used for <https://docs.astral.sh/ruff/settings/>.
use itertools::Itertools;
use ruff_server::ClientSettings;
use std::fmt::Write;
use ruff_python_trivia::textwrap;
@@ -15,12 +16,32 @@ pub(crate) fn generate() -> String {
&mut output,
Set::Toplevel(Options::metadata()),
&mut Vec::new(),
SetKind::Ruff,
);
output
}
fn generate_set(output: &mut String, set: Set, parents: &mut Vec<Set>) {
pub(crate) fn generate_server_options() -> String {
let mut output = String::new();
generate_set(
&mut output,
Set::Toplevel(ClientSettings::metadata()),
&mut Vec::new(),
SetKind::RuffServer,
);
output
}
#[derive(Copy, Clone)]
enum SetKind {
Ruff,
RuffServer,
}
fn generate_set(output: &mut String, set: Set, parents: &mut Vec<Set>, set_kind: SetKind) {
match &set {
Set::Toplevel(_) => {
output.push_str("### Top-level\n");
@@ -53,7 +74,7 @@ fn generate_set(output: &mut String, set: Set, parents: &mut Vec<Set>) {
// Generate the fields.
for (name, field) in &fields {
emit_field(output, name, field, parents.as_slice());
emit_field(output, name, field, parents.as_slice(), set_kind);
output.push_str("---\n\n");
}
@@ -66,6 +87,7 @@ fn generate_set(output: &mut String, set: Set, parents: &mut Vec<Set>) {
set: *sub_set,
},
parents,
set_kind,
);
}
@@ -93,7 +115,13 @@ impl Set {
}
}
fn emit_field(output: &mut String, name: &str, field: &OptionField, parents: &[Set]) {
fn emit_field(
output: &mut String,
name: &str,
field: &OptionField,
parents: &[Set],
set_kind: SetKind,
) {
let header_level = if parents.is_empty() { "####" } else { "#####" };
let parents_anchor = parents.iter().filter_map(|parent| parent.name()).join("_");
@@ -137,28 +165,46 @@ fn emit_field(output: &mut String, name: &str, field: &OptionField, parents: &[S
output.push_str(&format!("**Type**: `{}`\n", field.value_type));
output.push('\n');
output.push_str("**Example usage**:\n\n");
output.push_str(&format_tab(
"pyproject.toml",
&format_header(field.scope, parents, ConfigurationFile::PyprojectToml),
field.example,
));
output.push_str(&format_tab(
"ruff.toml",
&format_header(field.scope, parents, ConfigurationFile::RuffToml),
field.example,
));
match set_kind {
SetKind::Ruff => {
output.push_str(&format_tab(
"pyproject.toml",
&format_content(field, parents, ConfigurationFile::PyprojectToml),
));
output.push_str(&format_tab(
"ruff.toml",
&format_content(field, parents, ConfigurationFile::RuffToml),
));
}
SetKind::RuffServer => {}
}
output.push('\n');
}
fn format_tab(tab_name: &str, header: &str, content: &str) -> String {
fn format_tab(tab_name: &str, content: &str) -> String {
format!(
"=== \"{}\"\n\n ```toml\n {}\n{}\n ```\n",
"=== \"{}\"\n\n{}\n\n",
tab_name,
header,
textwrap::indent(content, " ")
)
}
fn format_content(
field: &OptionField,
parents: &[Set],
configuration: ConfigurationFile,
) -> String {
let header = format_header(field.scope, parents, configuration);
format!(
"```toml\n{}\n{}\n```",
header,
textwrap::indent(field.example, " ")
)
}
/// Format the TOML header for the example usage for a given option.
///
/// For example: `[tool.ruff.format]` or `[tool.ruff.lint.isort]`.
@@ -187,6 +233,15 @@ enum ConfigurationFile {
RuffToml,
}
fn format_server_content(field: &OptionField, editor: Editor) -> String {}
#[derive(Debug, Copy, Clone)]
enum Editor {
VSCode,
Neovim,
Zed,
}
#[derive(Default)]
struct CollectOptionsVisitor {
groups: Vec<(String, OptionSet)>,

View File

@@ -46,6 +46,8 @@ enum Command {
GenerateRulesTable,
/// Generate a Markdown-compatible listing of configuration options.
GenerateOptions,
/// Generate a Markdown-compatible listing of server options.
GenerateServerOptions,
/// Generate CLI help.
GenerateCliHelp(generate_cli_help::Args),
/// Generate Markdown docs.
@@ -89,6 +91,9 @@ fn main() -> Result<ExitCode> {
Command::GenerateKnotSchema(args) => generate_knot_schema::main(&args)?,
Command::GenerateRulesTable => println!("{}", generate_rules_table::generate()),
Command::GenerateOptions => println!("{}", generate_options::generate()),
Command::GenerateServerOptions => {
println!("{}", generate_options::generate_server_options())
}
Command::GenerateCliHelp(args) => generate_cli_help::main(&args)?,
Command::GenerateDocs(args) => generate_docs::main(&args)?,
Command::PrintAST(args) => print_ast::main(&args)?,

View File

@@ -24,19 +24,22 @@ pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result<TokenStream> {
}) => {
let mut output = vec![];
let rename_value =
RenameValue::from_attributes(struct_attributes.as_slice()).unwrap_or_default();
for field in &fields.named {
if let Some(attr) = field
.attrs
.iter()
.find(|attr| attr.path().is_ident("option"))
{
output.push(handle_option(field, attr)?);
output.push(handle_option(field, attr, rename_value)?);
} else if field
.attrs
.iter()
.any(|attr| attr.path().is_ident("option_group"))
{
output.push(handle_option_group(field)?);
output.push(handle_option_group(field, rename_value)?);
} else if let Some(serde) = field
.attrs
.iter()
@@ -86,8 +89,8 @@ pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result<TokenStream> {
Ok(quote! {
#[automatically_derived]
impl crate::options_base::OptionsMetadata for #ident {
fn record(visit: &mut dyn crate::options_base::Visit) {
impl ruff_workspace::options_base::OptionsMetadata for #ident {
fn record(visit: &mut dyn ruff_workspace::options_base::Visit) {
#(#output);*
}
@@ -105,7 +108,10 @@ pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result<TokenStream> {
/// For a field with type `Option<Foobar>` where `Foobar` itself is a struct
/// deriving `ConfigurationOptions`, create code that calls retrieves options
/// from that group: `Foobar::get_available_options()`
fn handle_option_group(field: &Field) -> syn::Result<proc_macro2::TokenStream> {
fn handle_option_group(
field: &Field,
rename_value: RenameValue,
) -> syn::Result<proc_macro2::TokenStream> {
let ident = field
.ident
.as_ref()
@@ -122,10 +128,10 @@ fn handle_option_group(field: &Field) -> syn::Result<proc_macro2::TokenStream> {
PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }),
}) if type_ident == "Option" => {
let path = &args[0];
let kebab_name = LitStr::new(&ident.to_string().replace('_', "-"), ident.span());
let renamed_field = rename_value.apply(ident);
Ok(quote_spanned!(
ident.span() => (visit.record_set(#kebab_name, crate::options_base::OptionSet::of::<#path>()))
ident.span() => (visit.record_set(#renamed_field, ruff_workspace::options_base::OptionSet::of::<#path>()))
))
}
_ => Err(syn::Error::new(
@@ -154,7 +160,11 @@ fn parse_doc(doc: &Attribute) -> syn::Result<String> {
/// Parse an `#[option(doc="...", default="...", value_type="...",
/// example="...")]` attribute and return data in the form of an `OptionField`.
fn handle_option(field: &Field, attr: &Attribute) -> syn::Result<proc_macro2::TokenStream> {
fn handle_option(
field: &Field,
attr: &Attribute,
rename_value: RenameValue,
) -> syn::Result<proc_macro2::TokenStream> {
let docs: Vec<&Attribute> = field
.attrs
.iter()
@@ -190,8 +200,7 @@ fn handle_option(field: &Field, attr: &Attribute) -> syn::Result<proc_macro2::To
example,
scope,
} = parse_field_attributes(attr)?;
let kebab_name = LitStr::new(&ident.to_string().replace('_', "-"), ident.span());
let renamed_field = rename_value.apply(ident);
let scope = if let Some(scope) = scope {
quote!(Some(#scope))
} else {
@@ -214,14 +223,14 @@ fn handle_option(field: &Field, attr: &Attribute) -> syn::Result<proc_macro2::To
let note = quote_option(deprecated.note);
let since = quote_option(deprecated.since);
quote!(Some(crate::options_base::Deprecated { since: #since, message: #note }))
quote!(Some(ruff_workspace::options_base::Deprecated { since: #since, message: #note }))
} else {
quote!(None)
};
Ok(quote_spanned!(
ident.span() => {
visit.record_field(#kebab_name, crate::options_base::OptionField{
visit.record_field(#renamed_field, ruff_workspace::options_base::OptionField{
doc: &#doc,
default: &#default,
value_type: &#value_type,
@@ -351,3 +360,66 @@ struct DeprecatedAttribute {
since: Option<String>,
note: Option<String>,
}
#[derive(Copy, Clone, Default)]
enum RenameValue {
#[default]
KebabCase,
CamelCase,
}
impl RenameValue {
fn from_attributes(attrs: &[Attribute]) -> Option<RenameValue> {
let serde = attrs.iter().find(|attr| attr.path().is_ident("serde"))?;
let Meta::List(list) = &serde.meta else {
return None;
};
let mut rename_value = None;
let _ = list.parse_nested_meta(|meta| {
if meta.path.is_ident("rename_all") {
let value = meta.value()?;
let s: LitStr = value.parse()?;
match s.value().as_str() {
"kebab-case" => {
rename_value = Some(RenameValue::KebabCase);
Ok(())
}
"camelCase" => {
rename_value = Some(RenameValue::CamelCase);
Ok(())
}
_ => Err(meta.error("Expected `kebab-case` or `camelCase`")),
}
} else {
Err(meta.error("Expected `rename_all`"))
}
});
rename_value
}
fn apply(self, ident: &syn::Ident) -> syn::LitStr {
let renamed = match self {
RenameValue::KebabCase => ident.to_string().replace('_', "-"),
RenameValue::CamelCase => {
let mut result = String::new();
let mut capitalize = false;
for c in ident.to_string().chars() {
if c == '_' {
capitalize = true;
} else if capitalize {
result.push(c.to_ascii_uppercase());
capitalize = false;
} else {
result.push(c);
}
}
result
}
};
LitStr::new(&renamed, ident.span())
}
}

View File

@@ -16,6 +16,7 @@ license = { workspace = true }
ruff_diagnostics = { workspace = true }
ruff_formatter = { workspace = true }
ruff_linter = { workspace = true }
ruff_macros = { workspace = true }
ruff_notebook = { workspace = true }
ruff_python_ast = { workspace = true }
ruff_python_codegen = { workspace = true }

View File

@@ -5,6 +5,8 @@ use rustc_hash::FxHashMap;
use serde::Deserialize;
use ruff_linter::{line_width::LineLength, RuleSelector};
use ruff_macros::OptionsMetadata;
use ruff_workspace::options_base::{OptionField, OptionsMetadata};
/// Maps a workspace URI to its associated client settings. Used during server initialization.
pub(crate) type WorkspaceSettingsMap = FxHashMap<Url, ClientSettings>;
@@ -57,27 +59,76 @@ pub(crate) enum ConfigurationPreference {
EditorOnly,
}
/// This is a direct representation of the settings schema sent by the client.
#[derive(Debug, Deserialize, Default)]
#[derive(Debug, Deserialize, Default, OptionsMetadata)]
#[cfg_attr(test, derive(PartialEq, Eq))]
#[serde(rename_all = "camelCase")]
pub struct ClientSettings {
/// Path to a `ruff.toml` or `pyproject.toml` file to use for configuration.
///
/// By default, Ruff will discover configuration for each project from the filesystem,
/// mirroring the behavior of the Ruff CLI.
#[option(
default = r#"null"#,
value_type = "string",
example = r#""~/path/to/ruff.toml""#
)]
configuration: Option<String>,
fix_all: Option<bool>,
organize_imports: Option<bool>,
lint: Option<LintOptions>,
format: Option<FormatOptions>,
code_action: Option<CodeActionOptions>,
exclude: Option<Vec<String>>,
line_length: Option<LineLength>,
/// The strategy to use when resolving settings across VS Code and the filesystem. By default,
/// editor configuration is prioritized over `ruff.toml` and `pyproject.toml` files.
///
/// * `"editorFirst"`: Editor settings take priority over configuration files present in the
/// workspace.
/// * `"filesystemFirst"`: Configuration files present in the workspace takes priority over
/// editor settings.
/// * `"editorOnly"`: Ignore configuration files entirely i.e., only use editor settings.
#[option(
default = r#""editorFirst""#,
value_type = r#""editorFirst" | "filesystemFirst" | "editorOnly""#,
example = r#""filesystemFirst""#
)]
configuration_preference: Option<ConfigurationPreference>,
/// If `true` or [`None`], show syntax errors as diagnostics.
/// A list of file patterns to exclude from linting and formatting. See [the
/// documentation](https://docs.astral.sh/ruff/settings/#exclude) for more details.
#[option(
default = r#"null"#,
value_type = "string[]",
example = r#"["**/tests/**"]"#
)]
exclude: Option<Vec<String>>,
/// The line length to use for the linter and formatter.
#[option(default = "null", value_type = "int", example = "100")]
line_length: Option<LineLength>,
/// Whether to register the server as capable of handling `source.fixAll` code actions.
#[option(default = "true", value_type = "bool", example = "false")]
fix_all: Option<bool>,
/// Whether to register the server as capable of handling `source.organizeImports` code
/// actions.
#[option(default = "true", value_type = "bool", example = "false")]
organize_imports: Option<bool>,
/// _New in Ruff [v0.5.0](https://astral.sh/blog/ruff-v0.5.0#changes-to-e999-and-reporting-of-syntax-errors)_
///
/// Whether to show syntax error diagnostics.
///
/// This is useful when using Ruff with other language servers, allowing the user to refer
/// to syntax errors from only one source.
#[option(default = "true", value_type = "bool", example = "false")]
show_syntax_errors: Option<bool>,
#[option_group]
lint: Option<LintOptions>,
#[option_group]
format: Option<FormatOptions>,
#[option_group]
code_action: Option<CodeActionOptions>,
// These settings are only needed for tracing, and are only read from the global configuration.
// These will not be in the resolved settings.
#[serde(flatten)]
@@ -98,14 +149,26 @@ impl ClientSettings {
}
}
/// Settings needed to initialize tracing. These will only be
/// read from the global configuration.
#[derive(Debug, Deserialize, Default)]
#[derive(Debug, Deserialize, Default, OptionsMetadata)]
#[cfg_attr(test, derive(PartialEq, Eq))]
#[serde(rename_all = "camelCase")]
pub(crate) struct TracingSettings {
/// The log level to use for the server.
#[option(
default = r#""info""#,
value_type = r#""error" | "warn" | "info" | "debug" | "trace""#,
example = r#""debug""#
)]
pub(crate) log_level: Option<crate::logging::LogLevel>,
/// Path to the log file - tildes and environment variables are supported.
/// Path to the log file to use for the server.
///
/// If not set, logs will be written to stderr. Tildes and environment variables are expanded.
#[option(
default = r#"null"#,
value_type = "string",
example = r#""~/path/to/ruff.log""#
)]
pub(crate) log_file: Option<PathBuf>,
}
@@ -121,14 +184,31 @@ struct WorkspaceSettings {
workspace: Url,
}
#[derive(Debug, Default, Deserialize)]
/// Settings specific to the Ruff linter.
#[derive(Debug, Default, Deserialize, OptionsMetadata)]
#[cfg_attr(test, derive(PartialEq, Eq))]
#[serde(rename_all = "camelCase")]
struct LintOptions {
/// Whether to enable linting. Set to `false` to use Ruff exclusively as a formatter.
#[option(default = "true", value_type = "bool", example = "false")]
enable: Option<bool>,
/// Whether to enable Ruff's preview mode when linting.
#[option(default = "null", value_type = "bool", example = "true")]
preview: Option<bool>,
/// Rules to enable by default. See [the
/// documentation](https://docs.astral.sh/ruff/settings/#lint_select).
#[option(default = "null", value_type = "string[]", example = r#"["E", "F"]"#)]
select: Option<Vec<String>>,
/// Rules to enable in addition to those in [`lint.select`](#select).
#[option(default = "null", value_type = "string[]", example = r#"["W"]"#)]
extend_select: Option<Vec<String>>,
/// Rules to disable by default. See [the
/// documentation](https://docs.astral.sh/ruff/settings/#lint_ignore).
#[option(default = "null", value_type = "string[]", example = r#"["E4", "E7"]"#)]
ignore: Option<Vec<String>>,
}
@@ -143,10 +223,13 @@ impl LintOptions {
}
}
#[derive(Debug, Default, Deserialize)]
/// Settings specific to the Ruff formatter.
#[derive(Debug, Default, Deserialize, OptionsMetadata)]
#[cfg_attr(test, derive(PartialEq, Eq))]
#[serde(rename_all = "camelCase")]
struct FormatOptions {
/// Whether to enable Ruff's preview mode when formatting.
#[option(default = "null", value_type = "bool", example = "true")]
preview: Option<bool>,
}
@@ -176,6 +259,38 @@ struct CodeActionParameters {
enable: Option<bool>,
}
impl OptionsMetadata for CodeActionOptions {
fn record(visit: &mut dyn ruff_workspace::options_base::Visit) {
visit.record_field(
"disableRuleComment.enable",
OptionField {
doc: "Whether to display Quick Fix actions to disable rules via `noqa` suppression comments.",
default: "true",
value_type: "bool",
scope: None,
example: "false",
deprecated: None,
},
);
visit.record_field(
"fixViolation.enable",
OptionField {
doc: "Whether to display Quick Fix actions to autofix violations.",
default: "true",
value_type: "bool",
scope: None,
example: "false",
deprecated: None,
},
);
}
fn documentation() -> Option<&'static str> {
Some("Enable or disable code actions provided by the server.")
}
}
/// This is the exact schema for initialization options sent in by the client
/// during initialization.
#[derive(Debug, Deserialize)]

View File

@@ -6,6 +6,7 @@ use std::collections::{BTreeMap, BTreeSet};
use std::path::PathBuf;
use strum::IntoEnumIterator;
use crate as ruff_workspace;
use crate::options_base::{OptionsMetadata, Visit};
use crate::settings::LineEnding;
use ruff_formatter::IndentStyle;