mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-13 07:08:03 +00:00
470 lines
14 KiB
Rust
470 lines
14 KiB
Rust
use std::fmt;
|
|
use std::path::Path;
|
|
use std::str::FromStr;
|
|
|
|
use ruff_formatter::printer::{LineEnding, PrinterOptions, SourceMapGeneration};
|
|
use ruff_formatter::{FormatOptions, IndentStyle, IndentWidth, LineWidth};
|
|
use ruff_macros::CacheKey;
|
|
use ruff_python_ast::{self as ast, PySourceType};
|
|
|
|
/// Resolved options for formatting one individual file. The difference to `FormatterSettings`
|
|
/// is that `FormatterSettings` stores the settings for multiple files (the entire project, a subdirectory, ..)
|
|
#[derive(Clone, Debug)]
|
|
#[cfg_attr(
|
|
feature = "serde",
|
|
derive(serde::Serialize, serde::Deserialize),
|
|
serde(default, deny_unknown_fields)
|
|
)]
|
|
pub struct PyFormatOptions {
|
|
/// Whether we're in a `.py` file or `.pyi` file, which have different rules.
|
|
source_type: PySourceType,
|
|
|
|
/// The (minimum) Python version used to run the formatted code. This is used
|
|
/// to determine the supported Python syntax.
|
|
target_version: ast::PythonVersion,
|
|
|
|
/// Specifies the indent style:
|
|
/// * Either a tab
|
|
/// * or a specific amount of spaces
|
|
#[cfg_attr(feature = "serde", serde(default = "default_indent_style"))]
|
|
indent_style: IndentStyle,
|
|
|
|
/// The preferred line width at which the formatter should wrap lines.
|
|
#[cfg_attr(feature = "serde", serde(default = "default_line_width"))]
|
|
line_width: LineWidth,
|
|
|
|
/// The visual width of a tab character.
|
|
#[cfg_attr(feature = "serde", serde(default = "default_indent_width"))]
|
|
indent_width: IndentWidth,
|
|
|
|
line_ending: LineEnding,
|
|
|
|
/// The preferred quote style to use (single vs double quotes).
|
|
quote_style: QuoteStyle,
|
|
|
|
/// Whether to expand lists or elements if they have a trailing comma such as `(a, b,)`.
|
|
magic_trailing_comma: MagicTrailingComma,
|
|
|
|
/// Should the formatter generate a source map that allows mapping source positions to positions
|
|
/// in the formatted document.
|
|
source_map_generation: SourceMapGeneration,
|
|
|
|
/// Whether to format code snippets in docstrings or not.
|
|
///
|
|
/// By default this is disabled (opt-in), but the plan is to make this
|
|
/// enabled by default (opt-out) in the future.
|
|
docstring_code: DocstringCode,
|
|
|
|
/// The preferred line width at which the formatter should wrap lines in
|
|
/// docstring code examples. This only has an impact when `docstring_code`
|
|
/// is enabled.
|
|
docstring_code_line_width: DocstringCodeLineWidth,
|
|
|
|
/// Whether preview style formatting is enabled or not
|
|
preview: PreviewMode,
|
|
}
|
|
|
|
fn default_line_width() -> LineWidth {
|
|
LineWidth::try_from(88).unwrap()
|
|
}
|
|
|
|
fn default_indent_style() -> IndentStyle {
|
|
IndentStyle::Space
|
|
}
|
|
|
|
fn default_indent_width() -> IndentWidth {
|
|
IndentWidth::try_from(4).unwrap()
|
|
}
|
|
|
|
impl Default for PyFormatOptions {
|
|
fn default() -> Self {
|
|
Self {
|
|
source_type: PySourceType::default(),
|
|
target_version: ast::PythonVersion::default(),
|
|
indent_style: default_indent_style(),
|
|
line_width: default_line_width(),
|
|
indent_width: default_indent_width(),
|
|
quote_style: QuoteStyle::default(),
|
|
line_ending: LineEnding::default(),
|
|
magic_trailing_comma: MagicTrailingComma::default(),
|
|
source_map_generation: SourceMapGeneration::default(),
|
|
docstring_code: DocstringCode::default(),
|
|
docstring_code_line_width: DocstringCodeLineWidth::default(),
|
|
preview: PreviewMode::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl PyFormatOptions {
|
|
/// Otherwise sets the defaults. Returns none if the extension is unknown
|
|
pub fn from_extension(path: &Path) -> Self {
|
|
Self::from_source_type(PySourceType::from(path))
|
|
}
|
|
|
|
pub fn from_source_type(source_type: PySourceType) -> Self {
|
|
Self {
|
|
source_type,
|
|
..Self::default()
|
|
}
|
|
}
|
|
|
|
pub const fn target_version(&self) -> ast::PythonVersion {
|
|
self.target_version
|
|
}
|
|
|
|
pub const fn magic_trailing_comma(&self) -> MagicTrailingComma {
|
|
self.magic_trailing_comma
|
|
}
|
|
|
|
pub const fn quote_style(&self) -> QuoteStyle {
|
|
self.quote_style
|
|
}
|
|
|
|
pub const fn source_type(&self) -> PySourceType {
|
|
self.source_type
|
|
}
|
|
|
|
pub const fn source_map_generation(&self) -> SourceMapGeneration {
|
|
self.source_map_generation
|
|
}
|
|
|
|
pub const fn line_ending(&self) -> LineEnding {
|
|
self.line_ending
|
|
}
|
|
|
|
pub const fn docstring_code(&self) -> DocstringCode {
|
|
self.docstring_code
|
|
}
|
|
|
|
pub const fn docstring_code_line_width(&self) -> DocstringCodeLineWidth {
|
|
self.docstring_code_line_width
|
|
}
|
|
|
|
pub const fn preview(&self) -> PreviewMode {
|
|
self.preview
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn with_target_version(mut self, target_version: ast::PythonVersion) -> Self {
|
|
self.target_version = target_version;
|
|
self
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn with_indent_width(mut self, indent_width: IndentWidth) -> Self {
|
|
self.indent_width = indent_width;
|
|
self
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn with_quote_style(mut self, style: QuoteStyle) -> Self {
|
|
self.quote_style = style;
|
|
self
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn with_magic_trailing_comma(mut self, trailing_comma: MagicTrailingComma) -> Self {
|
|
self.magic_trailing_comma = trailing_comma;
|
|
self
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn with_indent_style(mut self, indent_style: IndentStyle) -> Self {
|
|
self.indent_style = indent_style;
|
|
self
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn with_line_width(mut self, line_width: LineWidth) -> Self {
|
|
self.line_width = line_width;
|
|
self
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn with_line_ending(mut self, line_ending: LineEnding) -> Self {
|
|
self.line_ending = line_ending;
|
|
self
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn with_docstring_code(mut self, docstring_code: DocstringCode) -> Self {
|
|
self.docstring_code = docstring_code;
|
|
self
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn with_docstring_code_line_width(mut self, line_width: DocstringCodeLineWidth) -> Self {
|
|
self.docstring_code_line_width = line_width;
|
|
self
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn with_preview(mut self, preview: PreviewMode) -> Self {
|
|
self.preview = preview;
|
|
self
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn with_source_map_generation(mut self, source_map: SourceMapGeneration) -> Self {
|
|
self.source_map_generation = source_map;
|
|
self
|
|
}
|
|
}
|
|
|
|
impl FormatOptions for PyFormatOptions {
|
|
fn indent_style(&self) -> IndentStyle {
|
|
self.indent_style
|
|
}
|
|
|
|
fn indent_width(&self) -> IndentWidth {
|
|
self.indent_width
|
|
}
|
|
|
|
fn line_width(&self) -> LineWidth {
|
|
self.line_width
|
|
}
|
|
|
|
fn as_print_options(&self) -> PrinterOptions {
|
|
PrinterOptions {
|
|
indent_width: self.indent_width,
|
|
line_width: self.line_width,
|
|
line_ending: self.line_ending,
|
|
indent_style: self.indent_style,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, CacheKey)]
|
|
#[cfg_attr(
|
|
feature = "serde",
|
|
derive(serde::Serialize, serde::Deserialize),
|
|
serde(rename_all = "kebab-case")
|
|
)]
|
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
|
pub enum QuoteStyle {
|
|
Single,
|
|
#[default]
|
|
Double,
|
|
Preserve,
|
|
}
|
|
|
|
impl QuoteStyle {
|
|
pub const fn is_preserve(self) -> bool {
|
|
matches!(self, QuoteStyle::Preserve)
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for QuoteStyle {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
Self::Single => write!(f, "single"),
|
|
Self::Double => write!(f, "double"),
|
|
Self::Preserve => write!(f, "preserve"),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl FromStr for QuoteStyle {
|
|
type Err = &'static str;
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
match s {
|
|
"\"" | "double" | "Double" => Ok(Self::Double),
|
|
"'" | "single" | "Single" => Ok(Self::Single),
|
|
"preserve" | "Preserve" => Ok(Self::Preserve),
|
|
// TODO: replace this error with a diagnostic
|
|
_ => Err("Value not supported for QuoteStyle"),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Copy, Clone, Debug, Default, CacheKey)]
|
|
#[cfg_attr(
|
|
feature = "serde",
|
|
derive(serde::Serialize, serde::Deserialize),
|
|
serde(rename_all = "kebab-case")
|
|
)]
|
|
pub enum MagicTrailingComma {
|
|
#[default]
|
|
Respect,
|
|
Ignore,
|
|
}
|
|
|
|
impl MagicTrailingComma {
|
|
pub const fn is_respect(self) -> bool {
|
|
matches!(self, Self::Respect)
|
|
}
|
|
|
|
pub const fn is_ignore(self) -> bool {
|
|
matches!(self, Self::Ignore)
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for MagicTrailingComma {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
Self::Respect => write!(f, "respect"),
|
|
Self::Ignore => write!(f, "ignore"),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl FromStr for MagicTrailingComma {
|
|
type Err = &'static str;
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
match s {
|
|
"respect" | "Respect" => Ok(Self::Respect),
|
|
"ignore" | "Ignore" => Ok(Self::Ignore),
|
|
// TODO: replace this error with a diagnostic
|
|
_ => Err("Value not supported for MagicTrailingComma"),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, CacheKey)]
|
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
|
#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
|
|
pub enum PreviewMode {
|
|
#[default]
|
|
Disabled,
|
|
|
|
Enabled,
|
|
}
|
|
|
|
impl PreviewMode {
|
|
pub const fn is_enabled(self) -> bool {
|
|
matches!(self, PreviewMode::Enabled)
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for PreviewMode {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
Self::Disabled => write!(f, "disabled"),
|
|
Self::Enabled => write!(f, "enabled"),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, CacheKey)]
|
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
|
#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
|
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
|
pub enum DocstringCode {
|
|
#[default]
|
|
Disabled,
|
|
|
|
Enabled,
|
|
}
|
|
|
|
impl DocstringCode {
|
|
pub const fn is_enabled(self) -> bool {
|
|
matches!(self, DocstringCode::Enabled)
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for DocstringCode {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
Self::Disabled => write!(f, "disabled"),
|
|
Self::Enabled => write!(f, "enabled"),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Copy, Clone, Default, Eq, PartialEq, CacheKey)]
|
|
#[cfg_attr(
|
|
feature = "serde",
|
|
derive(serde::Serialize, serde::Deserialize),
|
|
serde(untagged, rename_all = "lowercase")
|
|
)]
|
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
|
pub enum DocstringCodeLineWidth {
|
|
/// Wrap docstring code examples at a fixed line width.
|
|
#[cfg_attr(feature = "schemars", schemars(schema_with = "schema::fixed"))]
|
|
Fixed(LineWidth),
|
|
|
|
/// Respect the line length limit setting for the surrounding Python code.
|
|
#[default]
|
|
#[cfg_attr(
|
|
feature = "serde",
|
|
serde(deserialize_with = "deserialize_docstring_code_line_width_dynamic")
|
|
)]
|
|
#[cfg_attr(feature = "schemars", schemars(schema_with = "schema::dynamic"))]
|
|
Dynamic,
|
|
}
|
|
|
|
#[cfg(feature = "schemars")]
|
|
mod schema {
|
|
use ruff_formatter::LineWidth;
|
|
use schemars::r#gen::SchemaGenerator;
|
|
use schemars::schema::{Metadata, Schema, SubschemaValidation};
|
|
|
|
/// A dummy type that is used to generate a schema for `DocstringCodeLineWidth::Dynamic`.
|
|
pub(super) fn dynamic(_: &mut SchemaGenerator) -> Schema {
|
|
Schema::Object(schemars::schema::SchemaObject {
|
|
const_value: Some("dynamic".to_string().into()),
|
|
..Default::default()
|
|
})
|
|
}
|
|
|
|
// We use a manual schema for `fixed` even thought it isn't strictly necessary according to the
|
|
// JSON schema specification to work around a bug in Even Better TOML with `allOf`.
|
|
// https://github.com/astral-sh/ruff/issues/15978#issuecomment-2639547101
|
|
//
|
|
// The only difference to the automatically derived schema is that we use `oneOf` instead of
|
|
// `allOf`. There's no semantic difference between `allOf` and `oneOf` for single element lists.
|
|
pub(super) fn fixed(generator: &mut SchemaGenerator) -> Schema {
|
|
let schema = generator.subschema_for::<LineWidth>();
|
|
Schema::Object(schemars::schema::SchemaObject {
|
|
metadata: Some(Box::new(Metadata {
|
|
description: Some(
|
|
"Wrap docstring code examples at a fixed line width.".to_string(),
|
|
),
|
|
..Metadata::default()
|
|
})),
|
|
subschemas: Some(Box::new(SubschemaValidation {
|
|
one_of: Some(vec![schema]),
|
|
..SubschemaValidation::default()
|
|
})),
|
|
..Default::default()
|
|
})
|
|
}
|
|
}
|
|
|
|
impl fmt::Debug for DocstringCodeLineWidth {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
match *self {
|
|
DocstringCodeLineWidth::Fixed(v) => v.value().fmt(f),
|
|
DocstringCodeLineWidth::Dynamic => "dynamic".fmt(f),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for DocstringCodeLineWidth {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
Self::Fixed(width) => width.fmt(f),
|
|
Self::Dynamic => write!(f, "dynamic"),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Responsible for deserializing the `DocstringCodeLineWidth::Dynamic`
|
|
/// variant.
|
|
fn deserialize_docstring_code_line_width_dynamic<'de, D>(d: D) -> Result<(), D::Error>
|
|
where
|
|
D: serde::Deserializer<'de>,
|
|
{
|
|
use serde::{Deserialize, de::Error};
|
|
|
|
let value = String::deserialize(d)?;
|
|
match &*value {
|
|
"dynamic" => Ok(()),
|
|
s => Err(D::Error::invalid_value(
|
|
serde::de::Unexpected::Str(s),
|
|
&"dynamic",
|
|
)),
|
|
}
|
|
}
|