diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3b17534533..0a4a153a11 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,6 +5,7 @@ exclude: | .github/workflows/release.yml| crates/ty_vendored/vendor/.*| crates/ty_project/resources/.*| + crates/ty/docs/configuration.md| crates/ty/docs/rules.md| crates/ruff_benchmark/resources/.*| crates/ruff_linter/resources/.*| diff --git a/Cargo.lock b/Cargo.lock index bbe15c5493..f4241ef97d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2583,6 +2583,7 @@ dependencies = [ "ruff_linter", "ruff_macros", "ruff_notebook", + "ruff_options_metadata", "ruff_python_ast", "ruff_python_formatter", "ruff_python_parser", @@ -2709,6 +2710,7 @@ dependencies = [ "ruff_formatter", "ruff_linter", "ruff_notebook", + "ruff_options_metadata", "ruff_python_ast", "ruff_python_codegen", "ruff_python_formatter", @@ -2876,6 +2878,13 @@ dependencies = [ "uuid", ] +[[package]] +name = "ruff_options_metadata" +version = "0.0.0" +dependencies = [ + "serde", +] + [[package]] name = "ruff_python_ast" version = "0.0.0" @@ -3162,6 +3171,7 @@ dependencies = [ "ruff_graph", "ruff_linter", "ruff_macros", + "ruff_options_metadata", "ruff_python_ast", "ruff_python_formatter", "ruff_python_semantic", @@ -4015,6 +4025,7 @@ dependencies = [ "ruff_cache", "ruff_db", "ruff_macros", + "ruff_options_metadata", "ruff_python_ast", "ruff_python_formatter", "ruff_text_size", diff --git a/Cargo.toml b/Cargo.toml index b47f364299..4f54929038 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ ruff_index = { path = "crates/ruff_index" } ruff_linter = { path = "crates/ruff_linter" } ruff_macros = { path = "crates/ruff_macros" } ruff_notebook = { path = "crates/ruff_notebook" } +ruff_options_metadata = { path = "crates/ruff_options_metadata" } ruff_python_ast = { path = "crates/ruff_python_ast" } ruff_python_codegen = { path = "crates/ruff_python_codegen" } ruff_python_formatter = { path = "crates/ruff_python_formatter" } @@ -182,7 +183,7 @@ wild = { version = "2" } zip = { version = "0.6.6", default-features = false } [workspace.metadata.cargo-shear] -ignored = ["getrandom"] +ignored = ["getrandom", "ruff_options_metadata"] [workspace.lints.rust] diff --git a/crates/ruff/Cargo.toml b/crates/ruff/Cargo.toml index 135c323df6..3a0fa79d49 100644 --- a/crates/ruff/Cargo.toml +++ b/crates/ruff/Cargo.toml @@ -20,6 +20,7 @@ ruff_graph = { workspace = true, features = ["serde", "clap"] } ruff_linter = { workspace = true, features = ["clap"] } ruff_macros = { workspace = true } ruff_notebook = { workspace = true } +ruff_options_metadata = { workspace = true, features = ["serde"] } ruff_python_ast = { workspace = true } ruff_python_formatter = { workspace = true } ruff_python_parser = { workspace = true } diff --git a/crates/ruff/src/args.rs b/crates/ruff/src/args.rs index 6fd5425a5a..fb2f9a273d 100644 --- a/crates/ruff/src/args.rs +++ b/crates/ruff/src/args.rs @@ -22,12 +22,12 @@ use ruff_linter::settings::types::{ PythonVersion, UnsafeFixes, }; use ruff_linter::{RuleParser, RuleSelector, RuleSelectorParser}; +use ruff_options_metadata::{OptionEntry, OptionsMetadata}; use ruff_python_ast as ast; use ruff_source_file::{LineIndex, OneIndexed, PositionEncoding}; use ruff_text_size::TextRange; use ruff_workspace::configuration::{Configuration, RuleSelection}; use ruff_workspace::options::{Options, PycodestyleOptions}; -use ruff_workspace::options_base::{OptionEntry, OptionsMetadata}; use ruff_workspace::resolver::ConfigurationTransformer; use rustc_hash::FxHashMap; use toml; diff --git a/crates/ruff/src/commands/completions/config.rs b/crates/ruff/src/commands/completions/config.rs index 3d6ddeadc8..9b9aa773da 100644 --- a/crates/ruff/src/commands/completions/config.rs +++ b/crates/ruff/src/commands/completions/config.rs @@ -2,10 +2,8 @@ use clap::builder::{PossibleValue, TypedValueParser, ValueParserFactory}; use itertools::Itertools; use std::str::FromStr; -use ruff_workspace::{ - options::Options, - options_base::{OptionField, OptionSet, OptionsMetadata, Visit}, -}; +use ruff_options_metadata::{OptionField, OptionSet, OptionsMetadata, Visit}; +use ruff_workspace::options::Options; #[derive(Default)] struct CollectOptionsVisitor { diff --git a/crates/ruff/src/commands/config.rs b/crates/ruff/src/commands/config.rs index 956c041e10..98bd9d70ae 100644 --- a/crates/ruff/src/commands/config.rs +++ b/crates/ruff/src/commands/config.rs @@ -2,8 +2,8 @@ use anyhow::{anyhow, Result}; use crate::args::HelpFormat; +use ruff_options_metadata::OptionsMetadata; use ruff_workspace::options::Options; -use ruff_workspace::options_base::OptionsMetadata; #[expect(clippy::print_stdout)] pub(crate) fn config(key: Option<&str>, format: HelpFormat) -> Result<()> { diff --git a/crates/ruff_dev/Cargo.toml b/crates/ruff_dev/Cargo.toml index e6194130f4..0bd71ca65c 100644 --- a/crates/ruff_dev/Cargo.toml +++ b/crates/ruff_dev/Cargo.toml @@ -17,6 +17,7 @@ ruff_diagnostics = { workspace = true } ruff_formatter = { workspace = true } ruff_linter = { workspace = true, features = ["schemars"] } ruff_notebook = { workspace = true } +ruff_options_metadata = { workspace = true } ruff_python_ast = { workspace = true } ruff_python_codegen = { workspace = true } ruff_python_formatter = { workspace = true } diff --git a/crates/ruff_dev/src/generate_all.rs b/crates/ruff_dev/src/generate_all.rs index 9a2963bb60..e04cdc891f 100644 --- a/crates/ruff_dev/src/generate_all.rs +++ b/crates/ruff_dev/src/generate_all.rs @@ -3,7 +3,8 @@ use anyhow::Result; use crate::{ - generate_cli_help, generate_docs, generate_json_schema, generate_ty_rules, generate_ty_schema, + generate_cli_help, generate_docs, generate_json_schema, generate_ty_options, generate_ty_rules, + generate_ty_schema, }; pub(crate) const REGENERATE_ALL_COMMAND: &str = "cargo dev generate-all"; @@ -40,6 +41,7 @@ pub(crate) fn main(args: &Args) -> Result<()> { generate_docs::main(&generate_docs::Args { dry_run: args.mode.is_dry_run(), })?; + generate_ty_options::main(&generate_ty_options::Args { mode: args.mode })?; generate_ty_rules::main(&generate_ty_rules::Args { mode: args.mode })?; Ok(()) } diff --git a/crates/ruff_dev/src/generate_docs.rs b/crates/ruff_dev/src/generate_docs.rs index 362ea4411f..e9b036144a 100644 --- a/crates/ruff_dev/src/generate_docs.rs +++ b/crates/ruff_dev/src/generate_docs.rs @@ -13,8 +13,8 @@ use strum::IntoEnumIterator; use ruff_diagnostics::FixAvailability; use ruff_linter::registry::{Linter, Rule, RuleNamespace}; +use ruff_options_metadata::{OptionEntry, OptionsMetadata}; use ruff_workspace::options::Options; -use ruff_workspace::options_base::{OptionEntry, OptionsMetadata}; use crate::ROOT_DIR; diff --git a/crates/ruff_dev/src/generate_options.rs b/crates/ruff_dev/src/generate_options.rs index 7c187878b8..8d6a4e956e 100644 --- a/crates/ruff_dev/src/generate_options.rs +++ b/crates/ruff_dev/src/generate_options.rs @@ -4,9 +4,9 @@ use itertools::Itertools; use std::fmt::Write; +use ruff_options_metadata::{OptionField, OptionSet, OptionsMetadata, Visit}; use ruff_python_trivia::textwrap; use ruff_workspace::options::Options; -use ruff_workspace::options_base::{OptionField, OptionSet, OptionsMetadata, Visit}; pub(crate) fn generate() -> String { let mut output = String::new(); diff --git a/crates/ruff_dev/src/generate_rules_table.rs b/crates/ruff_dev/src/generate_rules_table.rs index 16a7a48bf4..ec5be524e8 100644 --- a/crates/ruff_dev/src/generate_rules_table.rs +++ b/crates/ruff_dev/src/generate_rules_table.rs @@ -11,8 +11,8 @@ use strum::IntoEnumIterator; use ruff_diagnostics::FixAvailability; use ruff_linter::registry::{Linter, Rule, RuleNamespace}; use ruff_linter::upstream_categories::UpstreamCategoryAndPrefix; +use ruff_options_metadata::OptionsMetadata; use ruff_workspace::options::Options; -use ruff_workspace::options_base::OptionsMetadata; const FIX_SYMBOL: &str = "🛠️"; const PREVIEW_SYMBOL: &str = "🧪"; diff --git a/crates/ruff_dev/src/generate_ty_options.rs b/crates/ruff_dev/src/generate_ty_options.rs new file mode 100644 index 0000000000..1dc02d6e36 --- /dev/null +++ b/crates/ruff_dev/src/generate_ty_options.rs @@ -0,0 +1,258 @@ +//! Generate a Markdown-compatible listing of configuration options for `pyproject.toml`. +#![allow(clippy::print_stdout, clippy::print_stderr)] + +use anyhow::bail; +use itertools::Itertools; +use pretty_assertions::StrComparison; +use std::{fmt::Write, path::PathBuf}; + +use ruff_options_metadata::{OptionField, OptionSet, OptionsMetadata, Visit}; +use ty_project::metadata::Options; + +use crate::{ + generate_all::{Mode, REGENERATE_ALL_COMMAND}, + ROOT_DIR, +}; + +#[derive(clap::Args)] +pub(crate) struct Args { + /// Write the generated table to stdout (rather than to `crates/ty/docs/configuration.md`). + #[arg(long, default_value_t, value_enum)] + pub(crate) mode: Mode, +} + +pub(crate) fn main(args: &Args) -> anyhow::Result<()> { + let mut output = String::new(); + let file_name = "crates/ty/docs/configuration.md"; + let markdown_path = PathBuf::from(ROOT_DIR).join(file_name); + + generate_set( + &mut output, + Set::Toplevel(Options::metadata()), + &mut Vec::new(), + ); + + match args.mode { + Mode::DryRun => { + println!("{output}"); + } + Mode::Check => { + let current = std::fs::read_to_string(&markdown_path)?; + if output == current { + println!("Up-to-date: {file_name}",); + } else { + let comparison = StrComparison::new(¤t, &output); + bail!("{file_name} changed, please run `{REGENERATE_ALL_COMMAND}`:\n{comparison}",); + } + } + Mode::Write => { + let current = std::fs::read_to_string(&markdown_path)?; + if current == output { + println!("Up-to-date: {file_name}",); + } else { + println!("Updating: {file_name}",); + std::fs::write(markdown_path, output.as_bytes())?; + } + } + } + + Ok(()) +} + +fn generate_set(output: &mut String, set: Set, parents: &mut Vec) { + match &set { + Set::Toplevel(_) => { + output.push_str("# Configuration\n"); + } + Set::Named { name, .. } => { + let title = parents + .iter() + .filter_map(|set| set.name()) + .chain(std::iter::once(name.as_str())) + .join("."); + writeln!(output, "## `{title}`\n",).unwrap(); + } + } + + if let Some(documentation) = set.metadata().documentation() { + output.push_str(documentation); + output.push('\n'); + output.push('\n'); + } + + let mut visitor = CollectOptionsVisitor::default(); + set.metadata().record(&mut visitor); + + let (mut fields, mut sets) = (visitor.fields, visitor.groups); + + fields.sort_unstable_by(|(name, _), (name2, _)| name.cmp(name2)); + sets.sort_unstable_by(|(name, _), (name2, _)| name.cmp(name2)); + + parents.push(set); + + // Generate the fields. + for (name, field) in &fields { + emit_field(output, name, field, parents.as_slice()); + output.push_str("---\n\n"); + } + + // Generate all the sub-sets. + for (set_name, sub_set) in &sets { + generate_set( + output, + Set::Named { + name: set_name.to_string(), + set: *sub_set, + }, + parents, + ); + } + + parents.pop(); +} + +enum Set { + Toplevel(OptionSet), + Named { name: String, set: OptionSet }, +} + +impl Set { + fn name(&self) -> Option<&str> { + match self { + Set::Toplevel(_) => None, + Set::Named { name, .. } => Some(name), + } + } + + fn metadata(&self) -> &OptionSet { + match self { + Set::Toplevel(set) => set, + Set::Named { set, .. } => set, + } + } +} + +fn emit_field(output: &mut String, name: &str, field: &OptionField, parents: &[Set]) { + let header_level = if parents.is_empty() { "###" } else { "####" }; + + let _ = writeln!(output, "{header_level} [`{name}`]"); + + output.push('\n'); + + if let Some(deprecated) = &field.deprecated { + output.push_str("!!! warning \"Deprecated\"\n"); + output.push_str(" This option has been deprecated"); + + if let Some(since) = deprecated.since { + write!(output, " in {since}").unwrap(); + } + + output.push('.'); + + if let Some(message) = deprecated.message { + writeln!(output, " {message}").unwrap(); + } + + output.push('\n'); + } + + output.push_str(field.doc); + output.push_str("\n\n"); + let _ = writeln!(output, "**Default value**: `{}`", field.default); + output.push('\n'); + let _ = writeln!(output, "**Type**: `{}`", field.value_type); + output.push('\n'); + output.push_str("**Example usage** (`pyproject.toml`):\n\n"); + output.push_str(&format_example( + &format_header( + field.scope, + field.example, + parents, + ConfigurationFile::PyprojectToml, + ), + field.example, + )); + output.push('\n'); +} + +fn format_example(header: &str, content: &str) -> String { + if header.is_empty() { + format!("```toml\n{content}\n```\n",) + } else { + format!("```toml\n{header}\n{content}\n```\n",) + } +} + +/// Format the TOML header for the example usage for a given option. +/// +/// For example: `[tool.ruff.format]` or `[tool.ruff.lint.isort]`. +fn format_header( + scope: Option<&str>, + example: &str, + parents: &[Set], + configuration: ConfigurationFile, +) -> String { + let tool_parent = match configuration { + ConfigurationFile::PyprojectToml => Some("tool.ty"), + ConfigurationFile::TyToml => None, + }; + + let header = tool_parent + .into_iter() + .chain(parents.iter().filter_map(|parent| parent.name())) + .chain(scope) + .join("."); + + // Ex) `[[tool.ty.xx]]` + if example.starts_with(&format!("[[{header}")) { + return String::new(); + } + // Ex) `[tool.ty.rules]` + if example.starts_with(&format!("[{header}")) { + return String::new(); + } + + if header.is_empty() { + String::new() + } else { + format!("[{header}]") + } +} + +#[derive(Default)] +struct CollectOptionsVisitor { + groups: Vec<(String, OptionSet)>, + fields: Vec<(String, OptionField)>, +} + +impl Visit for CollectOptionsVisitor { + fn record_set(&mut self, name: &str, group: OptionSet) { + self.groups.push((name.to_owned(), group)); + } + + fn record_field(&mut self, name: &str, field: OptionField) { + self.fields.push((name.to_owned(), field)); + } +} + +#[derive(Debug, Copy, Clone)] +enum ConfigurationFile { + PyprojectToml, + #[expect(dead_code)] + TyToml, +} + +#[cfg(test)] +mod tests { + use anyhow::Result; + + use crate::generate_all::Mode; + + use super::{main, Args}; + + #[test] + fn ty_configuration_markdown_up_to_date() -> Result<()> { + main(&Args { mode: Mode::Check })?; + Ok(()) + } +} diff --git a/crates/ruff_dev/src/main.rs b/crates/ruff_dev/src/main.rs index cfbbd039fe..4ef20f8cd2 100644 --- a/crates/ruff_dev/src/main.rs +++ b/crates/ruff_dev/src/main.rs @@ -15,6 +15,7 @@ mod generate_docs; mod generate_json_schema; mod generate_options; mod generate_rules_table; +mod generate_ty_options; mod generate_ty_rules; mod generate_ty_schema; mod print_ast; @@ -48,6 +49,7 @@ enum Command { GenerateTyRules(generate_ty_rules::Args), /// Generate a Markdown-compatible listing of configuration options. GenerateOptions, + GenerateTyOptions(generate_ty_options::Args), /// Generate CLI help. GenerateCliHelp(generate_cli_help::Args), /// Generate Markdown docs. @@ -92,6 +94,7 @@ fn main() -> Result { Command::GenerateRulesTable => println!("{}", generate_rules_table::generate()), Command::GenerateTyRules(args) => generate_ty_rules::main(&args)?, Command::GenerateOptions => println!("{}", generate_options::generate()), + Command::GenerateTyOptions(args) => generate_ty_options::main(&args)?, Command::GenerateCliHelp(args) => generate_cli_help::main(&args)?, Command::GenerateDocs(args) => generate_docs::main(&args)?, Command::PrintAST(args) => print_ast::main(&args)?, diff --git a/crates/ruff_macros/src/config.rs b/crates/ruff_macros/src/config.rs index 0df56a2256..c5f9ba8aeb 100644 --- a/crates/ruff_macros/src/config.rs +++ b/crates/ruff_macros/src/config.rs @@ -86,8 +86,8 @@ pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result { Ok(quote! { #[automatically_derived] - impl crate::options_base::OptionsMetadata for #ident { - fn record(visit: &mut dyn crate::options_base::Visit) { + impl ruff_options_metadata::OptionsMetadata for #ident { + fn record(visit: &mut dyn ruff_options_metadata::Visit) { #(#output);* } @@ -125,7 +125,7 @@ fn handle_option_group(field: &Field) -> syn::Result { let kebab_name = LitStr::new(&ident.to_string().replace('_', "-"), ident.span()); Ok(quote_spanned!( - ident.span() => (visit.record_set(#kebab_name, crate::options_base::OptionSet::of::<#path>())) + ident.span() => (visit.record_set(#kebab_name, ruff_options_metadata::OptionSet::of::<#path>())) )) } _ => Err(syn::Error::new( @@ -214,14 +214,14 @@ fn handle_option(field: &Field, attr: &Attribute) -> syn::Result { - visit.record_field(#kebab_name, crate::options_base::OptionField{ + visit.record_field(#kebab_name, ruff_options_metadata::OptionField{ doc: &#doc, default: &#default, value_type: &#value_type, diff --git a/crates/ruff_options_metadata/Cargo.toml b/crates/ruff_options_metadata/Cargo.toml new file mode 100644 index 0000000000..55f3f4ad19 --- /dev/null +++ b/crates/ruff_options_metadata/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "ruff_options_metadata" +version = "0.0.0" +publish = false +authors = { workspace = true } +edition = { workspace = true } +rust-version = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +license = { workspace = true } + +[dependencies] +serde = { workspace = true, optional = true } + +[dev-dependencies] + +[lints] +workspace = true diff --git a/crates/ruff_workspace/src/options_base.rs b/crates/ruff_options_metadata/src/lib.rs similarity index 85% rename from crates/ruff_workspace/src/options_base.rs rename to crates/ruff_options_metadata/src/lib.rs index e3e4c650a2..7f7332c3c2 100644 --- a/crates/ruff_workspace/src/options_base.rs +++ b/crates/ruff_options_metadata/src/lib.rs @@ -1,6 +1,3 @@ -use serde::{Serialize, Serializer}; -use std::collections::BTreeMap; - use std::fmt::{Debug, Display, Formatter}; /// Visits [`OptionsMetadata`]. @@ -42,8 +39,8 @@ where } /// Metadata of an option that can either be a [`OptionField`] or [`OptionSet`]. -#[derive(Clone, PartialEq, Eq, Debug, Serialize)] -#[serde(untagged)] +#[derive(Clone, PartialEq, Eq, Debug)] +#[cfg_attr(feature = "serde", derive(::serde::Serialize), serde(untagged))] pub enum OptionEntry { /// A single option. Field(OptionField), @@ -102,7 +99,7 @@ impl OptionSet { /// ### Test for the existence of a child option /// /// ```rust - /// # use ruff_workspace::options_base::{OptionField, OptionsMetadata, Visit}; + /// # use ruff_options_metadata::{OptionField, OptionsMetadata, Visit}; /// /// struct WithOptions; /// @@ -125,7 +122,7 @@ impl OptionSet { /// ### Test for the existence of a nested option /// /// ```rust - /// # use ruff_workspace::options_base::{OptionField, OptionsMetadata, Visit}; + /// # use ruff_options_metadata::{OptionField, OptionsMetadata, Visit}; /// /// struct Root; /// @@ -176,7 +173,7 @@ impl OptionSet { /// ### Find a child option /// /// ```rust - /// # use ruff_workspace::options_base::{OptionEntry, OptionField, OptionsMetadata, Visit}; + /// # use ruff_options_metadata::{OptionEntry, OptionField, OptionsMetadata, Visit}; /// /// struct WithOptions; /// @@ -201,7 +198,7 @@ impl OptionSet { /// ### Find a nested option /// /// ```rust - /// # use ruff_workspace::options_base::{OptionEntry, OptionField, OptionsMetadata, Visit}; + /// # use ruff_options_metadata::{OptionEntry, OptionField, OptionsMetadata, Visit}; /// /// static HARD_TABS: OptionField = OptionField { /// doc: "Use hard tabs for indentation and spaces for alignment.", @@ -345,51 +342,14 @@ impl Display for OptionSet { } } -struct SerializeVisitor<'a> { - entries: &'a mut BTreeMap, -} - -impl Visit for SerializeVisitor<'_> { - fn record_set(&mut self, name: &str, set: OptionSet) { - // Collect the entries of the set. - let mut entries = BTreeMap::new(); - let mut visitor = SerializeVisitor { - entries: &mut entries, - }; - set.record(&mut visitor); - - // Insert the set into the entries. - for (key, value) in entries { - self.entries.insert(format!("{name}.{key}"), value); - } - } - - fn record_field(&mut self, name: &str, field: OptionField) { - self.entries.insert(name.to_string(), field); - } -} - -impl Serialize for OptionSet { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - let mut entries = BTreeMap::new(); - let mut visitor = SerializeVisitor { - entries: &mut entries, - }; - self.record(&mut visitor); - entries.serialize(serializer) - } -} - impl Debug for OptionSet { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { Display::fmt(self, f) } } -#[derive(Debug, Eq, PartialEq, Clone, Serialize)] +#[derive(Debug, Eq, PartialEq, Clone)] +#[cfg_attr(feature = "serde", derive(::serde::Serialize))] pub struct OptionField { pub doc: &'static str, /// Ex) `"false"` @@ -402,7 +362,8 @@ pub struct OptionField { pub deprecated: Option, } -#[derive(Debug, Clone, Eq, PartialEq, Serialize)] +#[derive(Debug, Clone, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(::serde::Serialize))] pub struct Deprecated { pub since: Option<&'static str>, pub message: Option<&'static str>, @@ -432,3 +393,48 @@ impl Display for OptionField { writeln!(f, "Example usage:\n```toml\n{}\n```", self.example) } } + +#[cfg(feature = "serde")] +mod serde { + use super::{OptionField, OptionSet, Visit}; + use serde::{Serialize, Serializer}; + use std::collections::BTreeMap; + + impl Serialize for OptionSet { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut entries = BTreeMap::new(); + let mut visitor = SerializeVisitor { + entries: &mut entries, + }; + self.record(&mut visitor); + entries.serialize(serializer) + } + } + + struct SerializeVisitor<'a> { + entries: &'a mut BTreeMap, + } + + impl Visit for SerializeVisitor<'_> { + fn record_set(&mut self, name: &str, set: OptionSet) { + // Collect the entries of the set. + let mut entries = BTreeMap::new(); + let mut visitor = SerializeVisitor { + entries: &mut entries, + }; + set.record(&mut visitor); + + // Insert the set into the entries. + for (key, value) in entries { + self.entries.insert(format!("{name}.{key}"), value); + } + } + + fn record_field(&mut self, name: &str, field: OptionField) { + self.entries.insert(name.to_string(), field); + } + } +} diff --git a/crates/ruff_python_ast/src/python_version.rs b/crates/ruff_python_ast/src/python_version.rs index 86977cf00f..63deafe713 100644 --- a/crates/ruff_python_ast/src/python_version.rs +++ b/crates/ruff_python_ast/src/python_version.rs @@ -60,6 +60,7 @@ impl PythonVersion { } pub const fn latest_ty() -> Self { + // Make sure to update the default value for `EnvironmentOptions::python_version` when bumping this version. Self::PY313 } diff --git a/crates/ruff_workspace/Cargo.toml b/crates/ruff_workspace/Cargo.toml index 5a185dc755..f1c8f09b83 100644 --- a/crates/ruff_workspace/Cargo.toml +++ b/crates/ruff_workspace/Cargo.toml @@ -18,6 +18,7 @@ ruff_formatter = { workspace = true } ruff_graph = { workspace = true, features = ["serde", "schemars"] } ruff_linter = { workspace = true } ruff_macros = { workspace = true } +ruff_options_metadata = { workspace = true } ruff_python_ast = { workspace = true } ruff_python_formatter = { workspace = true, features = ["serde"] } ruff_python_semantic = { workspace = true, features = ["serde"] } diff --git a/crates/ruff_workspace/src/lib.rs b/crates/ruff_workspace/src/lib.rs index 5307429613..bc8505c346 100644 --- a/crates/ruff_workspace/src/lib.rs +++ b/crates/ruff_workspace/src/lib.rs @@ -3,7 +3,6 @@ pub mod options; pub mod pyproject; pub mod resolver; -pub mod options_base; mod settings; pub use settings::{FileResolverSettings, FormatterSettings, Settings}; diff --git a/crates/ruff_workspace/src/options.rs b/crates/ruff_workspace/src/options.rs index eb4a49c93a..0bff30817b 100644 --- a/crates/ruff_workspace/src/options.rs +++ b/crates/ruff_workspace/src/options.rs @@ -6,7 +6,6 @@ use std::collections::{BTreeMap, BTreeSet}; use std::path::PathBuf; use strum::IntoEnumIterator; -use crate::options_base::{OptionsMetadata, Visit}; use crate::settings::LineEnding; use ruff_formatter::IndentStyle; use ruff_graph::Direction; @@ -32,6 +31,7 @@ use ruff_linter::settings::types::{ }; use ruff_linter::{warn_user_once, RuleSelector}; use ruff_macros::{CombineOptions, OptionsMetadata}; +use ruff_options_metadata::{OptionsMetadata, Visit}; use ruff_python_ast::name::Name; use ruff_python_formatter::{DocstringCodeLineWidth, QuoteStyle}; use ruff_python_semantic::NameImports; diff --git a/crates/ty/docs/configuration.md b/crates/ty/docs/configuration.md new file mode 100644 index 0000000000..896751f894 --- /dev/null +++ b/crates/ty/docs/configuration.md @@ -0,0 +1,220 @@ +# Configuration +#### [`respect-ignore-files`] + +Whether to automatically exclude files that are ignored by `.ignore`, +`.gitignore`, `.git/info/exclude`, and global `gitignore` files. +Enabled by default. + +**Default value**: `true` + +**Type**: `bool` + +**Example usage** (`pyproject.toml`): + +```toml +[tool.ty] +respect-ignore-files = false +``` + +--- + +#### [`rules`] + +Configures the enabled rules and their severity. + +See [the rules documentation](https://github.com/astral-sh/ruff/blob/main/crates/ty/docs/rules.md) for a list of all available rules. + +Valid severities are: + +* `ignore`: Disable the rule. +* `warn`: Enable the rule and create a warning diagnostic. +* `error`: Enable the rule and create an error diagnostic. + ty will exit with a non-zero code if any error diagnostics are emitted. + +**Default value**: `{...}` + +**Type**: `dict[RuleName, "ignore" | "warn" | "error"]` + +**Example usage** (`pyproject.toml`): + +```toml +[tool.ty.rules] +possibly-unresolved-reference = "warn" +division-by-zero = "ignore" +``` + +--- + +## `environment` + +#### [`extra-paths`] + +List of user-provided paths that should take first priority in the module resolution. +Examples in other type checkers are mypy's `MYPYPATH` environment variable, +or pyright's `stubPath` configuration setting. + +**Default value**: `[]` + +**Type**: `list[str]` + +**Example usage** (`pyproject.toml`): + +```toml +[tool.ty.environment] +extra-paths = ["~/shared/my-search-path"] +``` + +--- + +#### [`python`] + +Path to the Python installation from which ty resolves type information and third-party dependencies. + +ty will search in the path's `site-packages` directories for type information and +third-party imports. + +This option is commonly used to specify the path to a virtual environment. + +**Default value**: `null` + +**Type**: `str` + +**Example usage** (`pyproject.toml`): + +```toml +[tool.ty.environment] +python = "./.venv" +``` + +--- + +#### [`python-platform`] + +Specifies the target platform that will be used to analyze the source code. +If specified, ty will understand conditions based on comparisons with `sys.platform`, such +as are commonly found in typeshed to reflect the differing contents of the standard library across platforms. + +If no platform is specified, ty will use the current platform: +- `win32` for Windows +- `darwin` for macOS +- `android` for Android +- `ios` for iOS +- `linux` for everything else + +**Default value**: `` + +**Type**: `"win32" | "darwin" | "android" | "ios" | "linux" | str` + +**Example usage** (`pyproject.toml`): + +```toml +[tool.ty.environment] +# Tailor type stubs and conditionalized type definitions to windows. +python-platform = "win32" +``` + +--- + +#### [`python-version`] + +Specifies the version of Python that will be used to analyze the source code. +The version should be specified as a string in the format `M.m` where `M` is the major version +and `m` is the minor (e.g. `"3.0"` or `"3.6"`). +If a version is provided, ty will generate errors if the source code makes use of language features +that are not supported in that version. +It will also understand conditionals based on comparisons with `sys.version_info`, such +as are commonly found in typeshed to reflect the differing contents of the standard +library across Python versions. + +**Default value**: `"3.13"` + +**Type**: `"3.7" | "3.8" | "3.9" | "3.10" | "3.11" | "3.12" | "3.13" | .` + +**Example usage** (`pyproject.toml`): + +```toml +[tool.ty.environment] +python-version = "3.12" +``` + +--- + +#### [`typeshed`] + +Optional path to a "typeshed" directory on disk for us to use for standard-library types. +If this is not provided, we will fallback to our vendored typeshed stubs for the stdlib, +bundled as a zip file in the binary + +**Default value**: `null` + +**Type**: `str` + +**Example usage** (`pyproject.toml`): + +```toml +[tool.ty.environment] +typeshed = "/path/to/custom/typeshed" +``` + +--- + +## `src` + +#### [`root`] + +The root(s) of the project, used for finding first-party modules. + +**Default value**: `[".", "./src"]` + +**Type**: `list[str]` + +**Example usage** (`pyproject.toml`): + +```toml +[tool.ty.src] +root = ["./app"] +``` + +--- + +## `terminal` + +#### [`error-on-warning`] + +Use exit code 1 if there are any warning-level diagnostics. + +Defaults to `false`. + +**Default value**: `false` + +**Type**: `bool` + +**Example usage** (`pyproject.toml`): + +```toml +[tool.ty.terminal] +# Error if ty emits any warning-level diagnostics. +error-on-warning = true +``` + +--- + +#### [`output-format`] + +The format to use for printing diagnostic messages. + +Defaults to `full`. + +**Default value**: `full` + +**Type**: `full | concise` + +**Example usage** (`pyproject.toml`): + +```toml +[tool.ty.terminal] +output-format = "concise" +``` + +--- + diff --git a/crates/ty_project/Cargo.toml b/crates/ty_project/Cargo.toml index c05c3a4a4f..c6e30a6abc 100644 --- a/crates/ty_project/Cargo.toml +++ b/crates/ty_project/Cargo.toml @@ -15,6 +15,7 @@ license.workspace = true ruff_cache = { workspace = true } ruff_db = { workspace = true, features = ["cache", "serde"] } ruff_macros = { workspace = true } +ruff_options_metadata = { workspace = true } ruff_python_ast = { workspace = true, features = ["serde"] } ruff_python_formatter = { workspace = true, optional = true } ruff_text_size = { workspace = true } @@ -44,11 +45,7 @@ insta = { workspace = true, features = ["redactions", "ron"] } [features] default = ["zstd"] deflate = ["ty_vendored/deflate"] -schemars = [ - "dep:schemars", - "ruff_db/schemars", - "ty_python_semantic/schemars", -] +schemars = ["dep:schemars", "ruff_db/schemars", "ty_python_semantic/schemars"] zstd = ["ty_vendored/zstd"] format = ["ruff_python_formatter"] diff --git a/crates/ty_project/src/metadata.rs b/crates/ty_project/src/metadata.rs index c0061019eb..e2ba060b48 100644 --- a/crates/ty_project/src/metadata.rs +++ b/crates/ty_project/src/metadata.rs @@ -8,7 +8,7 @@ use ty_python_semantic::ProgramSettings; use crate::combine::Combine; use crate::metadata::pyproject::{Project, PyProject, PyProjectError, ResolveRequiresPythonError}; use crate::metadata::value::ValueSource; -use options::Options; +pub use options::Options; use options::TyTomlError; mod configuration_file; diff --git a/crates/ty_project/src/metadata/options.rs b/crates/ty_project/src/metadata/options.rs index cb25b6492a..aa3f24dc1e 100644 --- a/crates/ty_project/src/metadata/options.rs +++ b/crates/ty_project/src/metadata/options.rs @@ -3,7 +3,7 @@ use crate::Db; use ruff_db::diagnostic::{Annotation, Diagnostic, DiagnosticFormat, DiagnosticId, Severity, Span}; use ruff_db::files::system_path_to_file; use ruff_db::system::{System, SystemPath}; -use ruff_macros::Combine; +use ruff_macros::{Combine, OptionsMetadata}; use ruff_python_ast::PythonVersion; use rustc_hash::FxHashMap; use serde::{Deserialize, Serialize}; @@ -14,25 +14,57 @@ use ty_python_semantic::{ProgramSettings, PythonPath, PythonPlatform, SearchPath use super::settings::{Settings, TerminalSettings}; -/// The options for the project. -#[derive(Debug, Default, Clone, PartialEq, Eq, Combine, Serialize, Deserialize)] +#[derive( + Debug, Default, Clone, PartialEq, Eq, Combine, Serialize, Deserialize, OptionsMetadata, +)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct Options { /// Configures the type checking environment. + #[option_group] #[serde(skip_serializing_if = "Option::is_none")] pub environment: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[option_group] pub src: Option, - /// Configures the enabled lints and their severity. + /// Configures the enabled rules and their severity. + /// + /// See [the rules documentation](https://github.com/astral-sh/ruff/blob/main/crates/ty/docs/rules.md) for a list of all available rules. + /// + /// Valid severities are: + /// + /// * `ignore`: Disable the rule. + /// * `warn`: Enable the rule and create a warning diagnostic. + /// * `error`: Enable the rule and create an error diagnostic. + /// ty will exit with a non-zero code if any error diagnostics are emitted. #[serde(skip_serializing_if = "Option::is_none")] + #[option( + default = r#"{...}"#, + value_type = r#"dict[RuleName, "ignore" | "warn" | "error"]"#, + example = r#" + [tool.ty.rules] + possibly-unresolved-reference = "warn" + division-by-zero = "ignore" + "# + )] pub rules: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[option_group] pub terminal: Option, + /// Whether to automatically exclude files that are ignored by `.ignore`, + /// `.gitignore`, `.git/info/exclude`, and global `gitignore` files. + /// Enabled by default. + #[option( + default = r#"true"#, + value_type = r#"bool"#, + example = r#" + respect-ignore-files = false + "# + )] #[serde(skip_serializing_if = "Option::is_none")] pub respect_ignore_files: Option, } @@ -226,22 +258,33 @@ impl Options { } } -#[derive(Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize)] +#[derive( + Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize, OptionsMetadata, +)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct EnvironmentOptions { /// Specifies the version of Python that will be used to analyze the source code. /// The version should be specified as a string in the format `M.m` where `M` is the major version - /// and `m` is the minor (e.g. "3.0" or "3.6"). + /// and `m` is the minor (e.g. `"3.0"` or `"3.6"`). /// If a version is provided, ty will generate errors if the source code makes use of language features /// that are not supported in that version. - /// It will also tailor its use of type stub files, which conditionalizes type definitions based on the version. + /// It will also understand conditionals based on comparisons with `sys.version_info`, such + /// as are commonly found in typeshed to reflect the differing contents of the standard + /// library across Python versions. #[serde(skip_serializing_if = "Option::is_none")] + #[option( + default = r#""3.13""#, + value_type = r#""3.7" | "3.8" | "3.9" | "3.10" | "3.11" | "3.12" | "3.13" | ."#, + example = r#" + python-version = "3.12" + "# + )] pub python_version: Option>, /// Specifies the target platform that will be used to analyze the source code. - /// If specified, ty will tailor its use of type stub files, - /// which conditionalize type definitions based on the platform. + /// If specified, ty will understand conditions based on comparisons with `sys.platform`, such + /// as are commonly found in typeshed to reflect the differing contents of the standard library across platforms. /// /// If no platform is specified, ty will use the current platform: /// - `win32` for Windows @@ -250,18 +293,40 @@ pub struct EnvironmentOptions { /// - `ios` for iOS /// - `linux` for everything else #[serde(skip_serializing_if = "Option::is_none")] + #[option( + default = r#""#, + value_type = r#""win32" | "darwin" | "android" | "ios" | "linux" | str"#, + example = r#" + # Tailor type stubs and conditionalized type definitions to windows. + python-platform = "win32" + "# + )] pub python_platform: Option>, /// List of user-provided paths that should take first priority in the module resolution. - /// Examples in other type checkers are mypy's MYPYPATH environment variable, - /// or pyright's stubPath configuration setting. + /// Examples in other type checkers are mypy's `MYPYPATH` environment variable, + /// or pyright's `stubPath` configuration setting. #[serde(skip_serializing_if = "Option::is_none")] + #[option( + default = r#"[]"#, + value_type = "list[str]", + example = r#" + extra-paths = ["~/shared/my-search-path"] + "# + )] pub extra_paths: Option>, /// Optional path to a "typeshed" directory on disk for us to use for standard-library types. /// If this is not provided, we will fallback to our vendored typeshed stubs for the stdlib, /// bundled as a zip file in the binary #[serde(skip_serializing_if = "Option::is_none")] + #[option( + default = r#"null"#, + value_type = "str", + example = r#" + typeshed = "/path/to/custom/typeshed" + "# + )] pub typeshed: Option, /// Path to the Python installation from which ty resolves type information and third-party dependencies. @@ -271,15 +336,31 @@ pub struct EnvironmentOptions { /// /// This option is commonly used to specify the path to a virtual environment. #[serde(skip_serializing_if = "Option::is_none")] + #[option( + default = r#"null"#, + value_type = "str", + example = r#" + python = "./.venv" + "# + )] pub python: Option, } -#[derive(Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize)] +#[derive( + Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize, OptionsMetadata, +)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct SrcOptions { - /// The root of the project, used for finding first-party modules. + /// The root(s) of the project, used for finding first-party modules. #[serde(skip_serializing_if = "Option::is_none")] + #[option( + default = r#"[".", "./src"]"#, + value_type = "list[str]", + example = r#" + root = ["./app"] + "# + )] pub root: Option, } @@ -301,7 +382,9 @@ impl FromIterator<(RangedValue, RangedValue)> for Rules { } } -#[derive(Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize)] +#[derive( + Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize, OptionsMetadata, +)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct TerminalOptions { @@ -309,10 +392,25 @@ pub struct TerminalOptions { /// /// Defaults to `full`. #[serde(skip_serializing_if = "Option::is_none")] + #[option( + default = r#"full"#, + value_type = "full | concise", + example = r#" + output-format = "concise" + "# + )] pub output_format: Option>, /// Use exit code 1 if there are any warning-level diagnostics. /// /// Defaults to `false`. + #[option( + default = r#"false"#, + value_type = "bool", + example = r#" + # Error if ty emits any warning-level diagnostics. + error-on-warning = true + "# + )] pub error_on_warning: Option, } diff --git a/ty.schema.json b/ty.schema.json index 1d3f6b6f44..de51eec818 100644 --- a/ty.schema.json +++ b/ty.schema.json @@ -1,7 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "title": "Options", - "description": "The options for the project.", "type": "object", "properties": { "environment": { @@ -16,13 +15,14 @@ ] }, "respect-ignore-files": { + "description": "Whether to automatically exclude files that are ignored by `.ignore`, `.gitignore`, `.git/info/exclude`, and global `gitignore` files. Enabled by default.", "type": [ "boolean", "null" ] }, "rules": { - "description": "Configures the enabled lints and their severity.", + "description": "Configures the enabled rules and their severity.\n\nSee [the rules documentation](https://github.com/astral-sh/ruff/blob/main/crates/ty/docs/rules.md) for a list of all available rules.\n\nValid severities are:\n\n* `ignore`: Disable the rule. * `warn`: Enable the rule and create a warning diagnostic. * `error`: Enable the rule and create an error diagnostic. ty will exit with a non-zero code if any error diagnostics are emitted.", "anyOf": [ { "$ref": "#/definitions/Rules" @@ -78,7 +78,7 @@ "type": "object", "properties": { "extra-paths": { - "description": "List of user-provided paths that should take first priority in the module resolution. Examples in other type checkers are mypy's MYPYPATH environment variable, or pyright's stubPath configuration setting.", + "description": "List of user-provided paths that should take first priority in the module resolution. Examples in other type checkers are mypy's `MYPYPATH` environment variable, or pyright's `stubPath` configuration setting.", "type": [ "array", "null" @@ -95,7 +95,7 @@ ] }, "python-platform": { - "description": "Specifies the target platform that will be used to analyze the source code. If specified, ty will tailor its use of type stub files, which conditionalize type definitions based on the platform.\n\nIf no platform is specified, ty will use the current platform: - `win32` for Windows - `darwin` for macOS - `android` for Android - `ios` for iOS - `linux` for everything else", + "description": "Specifies the target platform that will be used to analyze the source code. If specified, ty will understand conditions based on comparisons with `sys.platform`, such as are commonly found in typeshed to reflect the differing contents of the standard library across platforms.\n\nIf no platform is specified, ty will use the current platform: - `win32` for Windows - `darwin` for macOS - `android` for Android - `ios` for iOS - `linux` for everything else", "anyOf": [ { "$ref": "#/definitions/PythonPlatform" @@ -106,7 +106,7 @@ ] }, "python-version": { - "description": "Specifies the version of Python that will be used to analyze the source code. The version should be specified as a string in the format `M.m` where `M` is the major version and `m` is the minor (e.g. \"3.0\" or \"3.6\"). If a version is provided, ty will generate errors if the source code makes use of language features that are not supported in that version. It will also tailor its use of type stub files, which conditionalizes type definitions based on the version.", + "description": "Specifies the version of Python that will be used to analyze the source code. The version should be specified as a string in the format `M.m` where `M` is the major version and `m` is the minor (e.g. `\"3.0\"` or `\"3.6\"`). If a version is provided, ty will generate errors if the source code makes use of language features that are not supported in that version. It will also understand conditionals based on comparisons with `sys.version_info`, such as are commonly found in typeshed to reflect the differing contents of the standard library across Python versions.", "anyOf": [ { "$ref": "#/definitions/PythonVersion" @@ -839,7 +839,7 @@ "type": "object", "properties": { "root": { - "description": "The root of the project, used for finding first-party modules.", + "description": "The root(s) of the project, used for finding first-party modules.", "type": [ "string", "null"