mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-28 04:45:01 +00:00

## Summary
This PR is another step in preparing to detect syntax errors in the
parser. It introduces the new `per-file-target-version` top-level
configuration option, which holds a mapping of compiled glob patterns to
Python versions. I intend to use the
`LinterSettings::resolve_target_version` method here to pass to the
parser:
f50849aeef/crates/ruff_linter/src/linter.rs (L491-L493)
## Test Plan
I added two new CLI tests to show that the `per-file-target-version` is
respected in both the formatter and the linter.
330 lines
11 KiB
Rust
330 lines
11 KiB
Rust
use path_absolutize::path_dedot;
|
|
use ruff_cache::cache_dir;
|
|
use ruff_formatter::{FormatOptions, IndentStyle, IndentWidth, LineWidth};
|
|
use ruff_graph::AnalyzeSettings;
|
|
use ruff_linter::display_settings;
|
|
use ruff_linter::settings::types::{
|
|
CompiledPerFileTargetVersionList, ExtensionMapping, FilePattern, FilePatternSet, OutputFormat,
|
|
UnsafeFixes,
|
|
};
|
|
use ruff_linter::settings::LinterSettings;
|
|
use ruff_macros::CacheKey;
|
|
use ruff_python_ast::{PySourceType, PythonVersion};
|
|
use ruff_python_formatter::{
|
|
DocstringCode, DocstringCodeLineWidth, MagicTrailingComma, PreviewMode, PyFormatOptions,
|
|
QuoteStyle,
|
|
};
|
|
use ruff_source_file::find_newline;
|
|
use std::fmt;
|
|
use std::path::{Path, PathBuf};
|
|
|
|
#[derive(Debug, CacheKey)]
|
|
#[allow(clippy::struct_excessive_bools)]
|
|
pub struct Settings {
|
|
#[cache_key(ignore)]
|
|
pub cache_dir: PathBuf,
|
|
#[cache_key(ignore)]
|
|
pub fix: bool,
|
|
#[cache_key(ignore)]
|
|
pub fix_only: bool,
|
|
#[cache_key(ignore)]
|
|
pub unsafe_fixes: UnsafeFixes,
|
|
#[cache_key(ignore)]
|
|
pub output_format: OutputFormat,
|
|
#[cache_key(ignore)]
|
|
pub show_fixes: bool,
|
|
|
|
pub file_resolver: FileResolverSettings,
|
|
pub linter: LinterSettings,
|
|
pub formatter: FormatterSettings,
|
|
pub analyze: AnalyzeSettings,
|
|
}
|
|
|
|
impl Default for Settings {
|
|
fn default() -> Self {
|
|
let project_root = path_dedot::CWD.as_path();
|
|
Self {
|
|
cache_dir: cache_dir(project_root),
|
|
fix: false,
|
|
fix_only: false,
|
|
output_format: OutputFormat::default(),
|
|
show_fixes: false,
|
|
unsafe_fixes: UnsafeFixes::default(),
|
|
linter: LinterSettings::new(project_root),
|
|
file_resolver: FileResolverSettings::new(project_root),
|
|
formatter: FormatterSettings::default(),
|
|
analyze: AnalyzeSettings::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for Settings {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
writeln!(f, "\n# General Settings")?;
|
|
display_settings! {
|
|
formatter = f,
|
|
fields = [
|
|
self.cache_dir | path,
|
|
self.fix,
|
|
self.fix_only,
|
|
self.output_format,
|
|
self.show_fixes,
|
|
self.unsafe_fixes,
|
|
self.file_resolver | nested,
|
|
self.linter | nested,
|
|
self.formatter | nested,
|
|
self.analyze | nested,
|
|
]
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, CacheKey)]
|
|
pub struct FileResolverSettings {
|
|
pub exclude: FilePatternSet,
|
|
pub extend_exclude: FilePatternSet,
|
|
pub force_exclude: bool,
|
|
pub include: FilePatternSet,
|
|
pub extend_include: FilePatternSet,
|
|
pub respect_gitignore: bool,
|
|
pub project_root: PathBuf,
|
|
}
|
|
|
|
impl fmt::Display for FileResolverSettings {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
writeln!(f, "\n# File Resolver Settings")?;
|
|
display_settings! {
|
|
formatter = f,
|
|
namespace = "file_resolver",
|
|
fields = [
|
|
self.exclude,
|
|
self.extend_exclude,
|
|
self.force_exclude,
|
|
self.include,
|
|
self.extend_include,
|
|
self.respect_gitignore,
|
|
self.project_root | path,
|
|
]
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
pub(crate) static EXCLUDE: &[FilePattern] = &[
|
|
FilePattern::Builtin(".bzr"),
|
|
FilePattern::Builtin(".direnv"),
|
|
FilePattern::Builtin(".eggs"),
|
|
FilePattern::Builtin(".git"),
|
|
FilePattern::Builtin(".git-rewrite"),
|
|
FilePattern::Builtin(".hg"),
|
|
FilePattern::Builtin(".ipynb_checkpoints"),
|
|
FilePattern::Builtin(".mypy_cache"),
|
|
FilePattern::Builtin(".nox"),
|
|
FilePattern::Builtin(".pants.d"),
|
|
FilePattern::Builtin(".pyenv"),
|
|
FilePattern::Builtin(".pytest_cache"),
|
|
FilePattern::Builtin(".pytype"),
|
|
FilePattern::Builtin(".ruff_cache"),
|
|
FilePattern::Builtin(".svn"),
|
|
FilePattern::Builtin(".tox"),
|
|
FilePattern::Builtin(".venv"),
|
|
FilePattern::Builtin(".vscode"),
|
|
FilePattern::Builtin("__pypackages__"),
|
|
FilePattern::Builtin("_build"),
|
|
FilePattern::Builtin("buck-out"),
|
|
FilePattern::Builtin("dist"),
|
|
FilePattern::Builtin("node_modules"),
|
|
FilePattern::Builtin("site-packages"),
|
|
FilePattern::Builtin("venv"),
|
|
];
|
|
|
|
pub(crate) static INCLUDE: &[FilePattern] = &[
|
|
FilePattern::Builtin("*.py"),
|
|
FilePattern::Builtin("*.pyi"),
|
|
FilePattern::Builtin("*.ipynb"),
|
|
FilePattern::Builtin("**/pyproject.toml"),
|
|
];
|
|
|
|
impl FileResolverSettings {
|
|
fn new(project_root: &Path) -> Self {
|
|
Self {
|
|
project_root: project_root.to_path_buf(),
|
|
exclude: FilePatternSet::try_from_iter(EXCLUDE.iter().cloned()).unwrap(),
|
|
extend_exclude: FilePatternSet::default(),
|
|
extend_include: FilePatternSet::default(),
|
|
force_exclude: false,
|
|
respect_gitignore: true,
|
|
include: FilePatternSet::try_from_iter(INCLUDE.iter().cloned()).unwrap(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(CacheKey, Clone, Debug)]
|
|
pub struct FormatterSettings {
|
|
pub exclude: FilePatternSet,
|
|
pub extension: ExtensionMapping,
|
|
pub preview: PreviewMode,
|
|
/// The non-path-resolved Python version specified by the `target-version` input option.
|
|
///
|
|
/// See [`FormatterSettings::resolve_target_version`] for a way to obtain the Python version for
|
|
/// a given file, while respecting the overrides in `per_file_target_version`.
|
|
pub unresolved_target_version: PythonVersion,
|
|
/// Path-specific overrides to `unresolved_target_version`.
|
|
///
|
|
/// See [`FormatterSettings::resolve_target_version`] for a way to check a given [`Path`]
|
|
/// against these patterns, while falling back to `unresolved_target_version` if none of them
|
|
/// match.
|
|
pub per_file_target_version: CompiledPerFileTargetVersionList,
|
|
|
|
pub line_width: LineWidth,
|
|
|
|
pub indent_style: IndentStyle,
|
|
pub indent_width: IndentWidth,
|
|
|
|
pub quote_style: QuoteStyle,
|
|
|
|
pub magic_trailing_comma: MagicTrailingComma,
|
|
|
|
pub line_ending: LineEnding,
|
|
|
|
pub docstring_code_format: DocstringCode,
|
|
pub docstring_code_line_width: DocstringCodeLineWidth,
|
|
}
|
|
|
|
impl FormatterSettings {
|
|
pub fn to_format_options(
|
|
&self,
|
|
source_type: PySourceType,
|
|
source: &str,
|
|
path: Option<&Path>,
|
|
) -> PyFormatOptions {
|
|
let target_version = path
|
|
.map(|path| self.resolve_target_version(path))
|
|
.unwrap_or(self.unresolved_target_version);
|
|
|
|
let line_ending = match self.line_ending {
|
|
LineEnding::Lf => ruff_formatter::printer::LineEnding::LineFeed,
|
|
LineEnding::CrLf => ruff_formatter::printer::LineEnding::CarriageReturnLineFeed,
|
|
#[cfg(target_os = "windows")]
|
|
LineEnding::Native => ruff_formatter::printer::LineEnding::CarriageReturnLineFeed,
|
|
#[cfg(not(target_os = "windows"))]
|
|
LineEnding::Native => ruff_formatter::printer::LineEnding::LineFeed,
|
|
LineEnding::Auto => match find_newline(source) {
|
|
Some((_, ruff_source_file::LineEnding::Lf)) => {
|
|
ruff_formatter::printer::LineEnding::LineFeed
|
|
}
|
|
Some((_, ruff_source_file::LineEnding::CrLf)) => {
|
|
ruff_formatter::printer::LineEnding::CarriageReturnLineFeed
|
|
}
|
|
Some((_, ruff_source_file::LineEnding::Cr)) => {
|
|
ruff_formatter::printer::LineEnding::CarriageReturn
|
|
}
|
|
None => ruff_formatter::printer::LineEnding::LineFeed,
|
|
},
|
|
};
|
|
|
|
PyFormatOptions::from_source_type(source_type)
|
|
.with_target_version(target_version)
|
|
.with_indent_style(self.indent_style)
|
|
.with_indent_width(self.indent_width)
|
|
.with_quote_style(self.quote_style)
|
|
.with_magic_trailing_comma(self.magic_trailing_comma)
|
|
.with_preview(self.preview)
|
|
.with_line_ending(line_ending)
|
|
.with_line_width(self.line_width)
|
|
.with_docstring_code(self.docstring_code_format)
|
|
.with_docstring_code_line_width(self.docstring_code_line_width)
|
|
}
|
|
|
|
/// Resolve the [`PythonVersion`] to use for formatting.
|
|
///
|
|
/// This method respects the per-file version overrides in
|
|
/// [`FormatterSettings::per_file_target_version`] and falls back on
|
|
/// [`FormatterSettings::unresolved_target_version`] if none of the override patterns match.
|
|
pub fn resolve_target_version(&self, path: &Path) -> PythonVersion {
|
|
self.per_file_target_version
|
|
.is_match(path)
|
|
.unwrap_or(self.unresolved_target_version)
|
|
}
|
|
}
|
|
|
|
impl Default for FormatterSettings {
|
|
fn default() -> Self {
|
|
let default_options = PyFormatOptions::default();
|
|
|
|
Self {
|
|
exclude: FilePatternSet::default(),
|
|
extension: ExtensionMapping::default(),
|
|
unresolved_target_version: default_options.target_version(),
|
|
per_file_target_version: CompiledPerFileTargetVersionList::default(),
|
|
preview: PreviewMode::Disabled,
|
|
line_width: default_options.line_width(),
|
|
line_ending: LineEnding::Auto,
|
|
indent_style: default_options.indent_style(),
|
|
indent_width: default_options.indent_width(),
|
|
quote_style: default_options.quote_style(),
|
|
magic_trailing_comma: default_options.magic_trailing_comma(),
|
|
docstring_code_format: default_options.docstring_code(),
|
|
docstring_code_line_width: default_options.docstring_code_line_width(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for FormatterSettings {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
writeln!(f, "\n# Formatter Settings")?;
|
|
display_settings! {
|
|
formatter = f,
|
|
namespace = "formatter",
|
|
fields = [
|
|
self.exclude,
|
|
self.unresolved_target_version,
|
|
self.per_file_target_version,
|
|
self.preview,
|
|
self.line_width,
|
|
self.line_ending,
|
|
self.indent_style,
|
|
self.indent_width,
|
|
self.quote_style,
|
|
self.magic_trailing_comma,
|
|
self.docstring_code_format,
|
|
self.docstring_code_line_width,
|
|
]
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(
|
|
Copy, Clone, Debug, Eq, PartialEq, Default, CacheKey, serde::Serialize, serde::Deserialize,
|
|
)]
|
|
#[serde(rename_all = "kebab-case")]
|
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
|
pub enum LineEnding {
|
|
/// The newline style is detected automatically on a file per file basis.
|
|
/// Files with mixed line endings will be converted to the first detected line ending.
|
|
/// Defaults to [`LineEnding::Lf`] for a files that contain no line endings.
|
|
#[default]
|
|
Auto,
|
|
|
|
/// Line endings will be converted to `\n` as is common on Unix.
|
|
Lf,
|
|
|
|
/// Line endings will be converted to `\r\n` as is common on Windows.
|
|
CrLf,
|
|
|
|
/// Line endings will be converted to `\n` on Unix and `\r\n` on Windows.
|
|
Native,
|
|
}
|
|
|
|
impl fmt::Display for LineEnding {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
Self::Auto => write!(f, "auto"),
|
|
Self::Lf => write!(f, "lf"),
|
|
Self::CrLf => write!(f, "crlf"),
|
|
Self::Native => write!(f, "native"),
|
|
}
|
|
}
|
|
}
|