ruff/crates/ruff_workspace/src/settings.rs
Charlie Marsh 4e935f7d7d
Add a subcommand to generate dependency graphs (#13402)
## Summary

This PR adds an experimental Ruff subcommand to generate dependency
graphs based on module resolution.

A few highlights:

- You can generate either dependency or dependent graphs via the
`--direction` command-line argument.
- Like Pants, we also provide an option to identify imports from string
literals (`--detect-string-imports`).
- Users can also provide additional dependency data via the
`include-dependencies` key under `[tool.ruff.import-map]`. This map uses
file paths as keys, and lists of strings as values. Those strings can be
file paths or globs.

The dependency resolution uses the red-knot module resolver which is
intended to be fully spec compliant, so it's also a chance to expose the
module resolver in a real-world setting.

The CLI is, e.g., `ruff graph build ../autobot`, which will output a
JSON map from file to files it depends on for the `autobot` project.
2024-09-19 21:06:32 -04:00

297 lines
9.9 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::{
ExtensionMapping, FilePattern, FilePatternSet, OutputFormat, UnsafeFixes,
};
use ruff_linter::settings::LinterSettings;
use ruff_macros::CacheKey;
use ruff_python_ast::PySourceType;
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,
pub target_version: ruff_python_formatter::PythonVersion,
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) -> PyFormatOptions {
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(self.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)
}
}
impl Default for FormatterSettings {
fn default() -> Self {
let default_options = PyFormatOptions::default();
Self {
exclude: FilePatternSet::default(),
extension: ExtensionMapping::default(),
target_version: default_options.target_version(),
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.target_version | debug,
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"),
}
}
}