mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-04 18:58:04 +00:00
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.
This commit is contained in:
parent
260c2ecd15
commit
4e935f7d7d
30 changed files with 1339 additions and 45 deletions
98
Cargo.lock
generated
98
Cargo.lock
generated
|
@ -161,6 +161,21 @@ version = "0.7.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
|
||||
|
||||
[[package]]
|
||||
name = "assert_fs"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7efdb1fdb47602827a342857666feb372712cbc64b414172bd6b167a02927674"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"doc-comment",
|
||||
"globwalk",
|
||||
"predicates",
|
||||
"predicates-core",
|
||||
"predicates-tree",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.2.0"
|
||||
|
@ -240,6 +255,9 @@ name = "camino"
|
|||
version = "1.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cast"
|
||||
|
@ -722,6 +740,12 @@ version = "0.1.13"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
|
||||
|
||||
[[package]]
|
||||
name = "difflib"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
|
@ -773,6 +797,12 @@ dependencies = [
|
|||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "doc-comment"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
|
||||
|
||||
[[package]]
|
||||
name = "drop_bomb"
|
||||
version = "0.1.5"
|
||||
|
@ -968,6 +998,17 @@ dependencies = [
|
|||
"regex-syntax 0.8.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "globwalk"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"ignore",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "half"
|
||||
version = "2.4.1"
|
||||
|
@ -1864,6 +1905,33 @@ version = "0.2.17"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
|
||||
|
||||
[[package]]
|
||||
name = "predicates"
|
||||
version = "3.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e9086cc7640c29a356d1a29fd134380bee9d8f79a17410aa76e7ad295f42c97"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"difflib",
|
||||
"predicates-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "predicates-core"
|
||||
version = "1.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae8177bee8e75d6846599c6b9ff679ed51e882816914eec639944d7c9aa11931"
|
||||
|
||||
[[package]]
|
||||
name = "predicates-tree"
|
||||
version = "1.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41b740d195ed3166cd147c8047ec98db0e22ec019eb8eeb76d343b795304fb13"
|
||||
dependencies = [
|
||||
"predicates-core",
|
||||
"termtree",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pretty_assertions"
|
||||
version = "1.4.0"
|
||||
|
@ -2191,6 +2259,7 @@ version = "0.6.5"
|
|||
dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
"assert_fs",
|
||||
"bincode",
|
||||
"bitflags 2.6.0",
|
||||
"cachedir",
|
||||
|
@ -2200,7 +2269,9 @@ dependencies = [
|
|||
"clearscreen",
|
||||
"colored",
|
||||
"filetime",
|
||||
"globwalk",
|
||||
"ignore",
|
||||
"indoc",
|
||||
"insta",
|
||||
"insta-cmd",
|
||||
"is-macro",
|
||||
|
@ -2212,7 +2283,9 @@ dependencies = [
|
|||
"rayon",
|
||||
"regex",
|
||||
"ruff_cache",
|
||||
"ruff_db",
|
||||
"ruff_diagnostics",
|
||||
"ruff_graph",
|
||||
"ruff_linter",
|
||||
"ruff_macros",
|
||||
"ruff_notebook",
|
||||
|
@ -2295,6 +2368,7 @@ dependencies = [
|
|||
"ruff_text_size",
|
||||
"rustc-hash 2.0.0",
|
||||
"salsa",
|
||||
"serde",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"tracing",
|
||||
|
@ -2370,6 +2444,23 @@ dependencies = [
|
|||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff_graph"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"red_knot_python_semantic",
|
||||
"ruff_cache",
|
||||
"ruff_db",
|
||||
"ruff_linter",
|
||||
"ruff_macros",
|
||||
"ruff_python_ast",
|
||||
"salsa",
|
||||
"schemars",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff_index"
|
||||
version = "0.0.0"
|
||||
|
@ -2743,6 +2834,7 @@ dependencies = [
|
|||
"regex",
|
||||
"ruff_cache",
|
||||
"ruff_formatter",
|
||||
"ruff_graph",
|
||||
"ruff_linter",
|
||||
"ruff_macros",
|
||||
"ruff_python_ast",
|
||||
|
@ -3197,6 +3289,12 @@ dependencies = [
|
|||
"phf_codegen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termtree"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76"
|
||||
|
||||
[[package]]
|
||||
name = "test-case"
|
||||
version = "3.3.1"
|
||||
|
|
|
@ -17,6 +17,7 @@ ruff_cache = { path = "crates/ruff_cache" }
|
|||
ruff_db = { path = "crates/ruff_db" }
|
||||
ruff_diagnostics = { path = "crates/ruff_diagnostics" }
|
||||
ruff_formatter = { path = "crates/ruff_formatter" }
|
||||
ruff_graph = { path = "crates/ruff_graph" }
|
||||
ruff_index = { path = "crates/ruff_index" }
|
||||
ruff_linter = { path = "crates/ruff_linter" }
|
||||
ruff_macros = { path = "crates/ruff_macros" }
|
||||
|
@ -42,6 +43,7 @@ red_knot_workspace = { path = "crates/red_knot_workspace" }
|
|||
aho-corasick = { version = "1.1.3" }
|
||||
annotate-snippets = { version = "0.9.2", features = ["color"] }
|
||||
anyhow = { version = "1.0.80" }
|
||||
assert_fs = { version = "1.1.0" }
|
||||
argfile = { version = "0.2.0" }
|
||||
bincode = { version = "1.3.3" }
|
||||
bitflags = { version = "2.5.0" }
|
||||
|
@ -68,6 +70,7 @@ fern = { version = "0.6.1" }
|
|||
filetime = { version = "0.2.23" }
|
||||
glob = { version = "0.3.1" }
|
||||
globset = { version = "0.4.14" }
|
||||
globwalk = { version = "0.9.1" }
|
||||
hashbrown = "0.14.3"
|
||||
ignore = { version = "0.4.22" }
|
||||
imara-diff = { version = "0.1.5" }
|
||||
|
|
|
@ -4,7 +4,9 @@ use rustc_hash::FxHasher;
|
|||
|
||||
pub use db::Db;
|
||||
pub use module_name::ModuleName;
|
||||
pub use module_resolver::{resolve_module, system_module_search_paths, vendored_typeshed_stubs};
|
||||
pub use module_resolver::{
|
||||
resolve_module, system_module_search_paths, vendored_typeshed_stubs, Module,
|
||||
};
|
||||
pub use program::{Program, ProgramSettings, SearchPathSettings, SitePackages};
|
||||
pub use python_version::PythonVersion;
|
||||
pub use semantic_model::{HasTy, SemanticModel};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use std::iter::FusedIterator;
|
||||
|
||||
pub(crate) use module::Module;
|
||||
pub use module::Module;
|
||||
pub use resolver::resolve_module;
|
||||
pub(crate) use resolver::{file_to_module, SearchPaths};
|
||||
use ruff_db::system::SystemPath;
|
||||
|
|
|
@ -14,7 +14,9 @@ default-run = "ruff"
|
|||
|
||||
[dependencies]
|
||||
ruff_cache = { workspace = true }
|
||||
ruff_db = { workspace = true }
|
||||
ruff_diagnostics = { workspace = true }
|
||||
ruff_graph = { workspace = true, features = ["serde", "clap"] }
|
||||
ruff_linter = { workspace = true, features = ["clap"] }
|
||||
ruff_macros = { workspace = true }
|
||||
ruff_notebook = { workspace = true }
|
||||
|
@ -36,6 +38,7 @@ clap_complete_command = { workspace = true }
|
|||
clearscreen = { workspace = true }
|
||||
colored = { workspace = true }
|
||||
filetime = { workspace = true }
|
||||
globwalk = { workspace = true }
|
||||
ignore = { workspace = true }
|
||||
is-macro = { workspace = true }
|
||||
itertools = { workspace = true }
|
||||
|
@ -59,8 +62,11 @@ wild = { workspace = true }
|
|||
[dev-dependencies]
|
||||
# Enable test rules during development
|
||||
ruff_linter = { workspace = true, features = ["clap", "test-rules"] }
|
||||
|
||||
assert_fs = { workspace = true }
|
||||
# Avoid writing colored snapshots when running tests from the terminal
|
||||
colored = { workspace = true, features = ["no-color"] }
|
||||
indoc = { workspace = true }
|
||||
insta = { workspace = true, features = ["filters", "json"] }
|
||||
insta-cmd = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
|
|
|
@ -7,13 +7,11 @@ use std::sync::Arc;
|
|||
|
||||
use anyhow::{anyhow, bail};
|
||||
use clap::builder::{TypedValueParser, ValueParserFactory};
|
||||
use clap::{command, Parser};
|
||||
use clap::{command, Parser, Subcommand};
|
||||
use colored::Colorize;
|
||||
use path_absolutize::path_dedot;
|
||||
use regex::Regex;
|
||||
use rustc_hash::FxHashMap;
|
||||
use toml;
|
||||
|
||||
use ruff_graph::Direction;
|
||||
use ruff_linter::line_width::LineLength;
|
||||
use ruff_linter::logging::LogLevel;
|
||||
use ruff_linter::registry::Rule;
|
||||
|
@ -27,6 +25,8 @@ use ruff_text_size::TextRange;
|
|||
use ruff_workspace::configuration::{Configuration, RuleSelection};
|
||||
use ruff_workspace::options::{Options, PycodestyleOptions};
|
||||
use ruff_workspace::resolver::ConfigurationTransformer;
|
||||
use rustc_hash::FxHashMap;
|
||||
use toml;
|
||||
|
||||
/// All configuration options that can be passed "globally",
|
||||
/// i.e., can be passed to all subcommands
|
||||
|
@ -132,6 +132,9 @@ pub enum Command {
|
|||
Format(FormatCommand),
|
||||
/// Run the language server.
|
||||
Server(ServerCommand),
|
||||
/// Run analysis over Python source code.
|
||||
#[clap(subcommand)]
|
||||
Analyze(AnalyzeCommand),
|
||||
/// Display Ruff's version
|
||||
Version {
|
||||
#[arg(long, value_enum, default_value = "text")]
|
||||
|
@ -139,6 +142,32 @@ pub enum Command {
|
|||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum AnalyzeCommand {
|
||||
/// Generate a map of Python file dependencies or dependents.
|
||||
Graph(AnalyzeGraphCommand),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, clap::Parser)]
|
||||
pub struct AnalyzeGraphCommand {
|
||||
/// List of files or directories to include.
|
||||
#[clap(help = "List of files or directories to include [default: .]")]
|
||||
pub files: Vec<PathBuf>,
|
||||
/// The direction of the import map. By default, generates a dependency map, i.e., a map from
|
||||
/// file to files that it depends on. Use `--direction dependents` to generate a map from file
|
||||
/// to files that depend on it.
|
||||
#[clap(long, value_enum, default_value_t)]
|
||||
pub direction: Direction,
|
||||
/// Attempt to detect imports from string literals.
|
||||
#[clap(long)]
|
||||
pub detect_string_imports: bool,
|
||||
/// Enable preview mode. Use `--no-preview` to disable.
|
||||
#[arg(long, overrides_with("no_preview"))]
|
||||
preview: bool,
|
||||
#[clap(long, overrides_with("preview"), hide = true)]
|
||||
no_preview: bool,
|
||||
}
|
||||
|
||||
// The `Parser` derive is for ruff_dev, for ruff `Args` would be sufficient
|
||||
#[derive(Clone, Debug, clap::Parser)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
|
@ -700,6 +729,7 @@ impl CheckCommand {
|
|||
output_format: resolve_output_format(self.output_format)?,
|
||||
show_fixes: resolve_bool_arg(self.show_fixes, self.no_show_fixes),
|
||||
extension: self.extension,
|
||||
..ExplicitConfigOverrides::default()
|
||||
};
|
||||
|
||||
let config_args = ConfigArguments::from_cli_arguments(global_options, cli_overrides)?;
|
||||
|
@ -732,8 +762,33 @@ impl FormatCommand {
|
|||
target_version: self.target_version,
|
||||
cache_dir: self.cache_dir,
|
||||
extension: self.extension,
|
||||
..ExplicitConfigOverrides::default()
|
||||
};
|
||||
|
||||
// Unsupported on the formatter CLI, but required on `Overrides`.
|
||||
let config_args = ConfigArguments::from_cli_arguments(global_options, cli_overrides)?;
|
||||
Ok((format_arguments, config_args))
|
||||
}
|
||||
}
|
||||
|
||||
impl AnalyzeGraphCommand {
|
||||
/// Partition the CLI into command-line arguments and configuration
|
||||
/// overrides.
|
||||
pub fn partition(
|
||||
self,
|
||||
global_options: GlobalConfigArgs,
|
||||
) -> anyhow::Result<(AnalyzeGraphArgs, ConfigArguments)> {
|
||||
let format_arguments = AnalyzeGraphArgs {
|
||||
files: self.files,
|
||||
direction: self.direction,
|
||||
};
|
||||
|
||||
let cli_overrides = ExplicitConfigOverrides {
|
||||
detect_string_imports: if self.detect_string_imports {
|
||||
Some(true)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
preview: resolve_bool_arg(self.preview, self.no_preview).map(PreviewMode::from),
|
||||
..ExplicitConfigOverrides::default()
|
||||
};
|
||||
|
||||
|
@ -896,7 +951,7 @@ A `--config` flag must either be a path to a `.toml` configuration file
|
|||
// the user was trying to pass in a path to a configuration file
|
||||
// or some inline TOML.
|
||||
// We want to display the most helpful error to the user as possible.
|
||||
if std::path::Path::new(value)
|
||||
if Path::new(value)
|
||||
.extension()
|
||||
.map_or(false, |ext| ext.eq_ignore_ascii_case("toml"))
|
||||
{
|
||||
|
@ -1156,6 +1211,13 @@ impl LineColumnParseError {
|
|||
}
|
||||
}
|
||||
|
||||
/// CLI settings that are distinct from configuration (commands, lists of files, etc.).
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AnalyzeGraphArgs {
|
||||
pub files: Vec<PathBuf>,
|
||||
pub direction: Direction,
|
||||
}
|
||||
|
||||
/// Configuration overrides provided via dedicated CLI flags:
|
||||
/// `--line-length`, `--respect-gitignore`, etc.
|
||||
#[derive(Clone, Default)]
|
||||
|
@ -1187,6 +1249,7 @@ struct ExplicitConfigOverrides {
|
|||
output_format: Option<OutputFormat>,
|
||||
show_fixes: Option<bool>,
|
||||
extension: Option<Vec<ExtensionPair>>,
|
||||
detect_string_imports: Option<bool>,
|
||||
}
|
||||
|
||||
impl ConfigurationTransformer for ExplicitConfigOverrides {
|
||||
|
@ -1271,6 +1334,9 @@ impl ConfigurationTransformer for ExplicitConfigOverrides {
|
|||
if let Some(extension) = &self.extension {
|
||||
config.extension = Some(extension.iter().cloned().collect());
|
||||
}
|
||||
if let Some(detect_string_imports) = &self.detect_string_imports {
|
||||
config.analyze.detect_string_imports = Some(*detect_string_imports);
|
||||
}
|
||||
|
||||
config
|
||||
}
|
||||
|
|
182
crates/ruff/src/commands/analyze_graph.rs
Normal file
182
crates/ruff/src/commands/analyze_graph.rs
Normal file
|
@ -0,0 +1,182 @@
|
|||
use crate::args::{AnalyzeGraphArgs, ConfigArguments};
|
||||
use crate::resolve::resolve;
|
||||
use crate::{resolve_default_files, ExitStatus};
|
||||
use anyhow::Result;
|
||||
use log::{debug, warn};
|
||||
use path_absolutize::CWD;
|
||||
use ruff_db::system::{SystemPath, SystemPathBuf};
|
||||
use ruff_graph::{Direction, ImportMap, ModuleDb, ModuleImports};
|
||||
use ruff_linter::{warn_user, warn_user_once};
|
||||
use ruff_python_ast::{PySourceType, SourceType};
|
||||
use ruff_workspace::resolver::{python_files_in_path, ResolvedFile};
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Generate an import map.
|
||||
pub(crate) fn analyze_graph(
|
||||
args: AnalyzeGraphArgs,
|
||||
config_arguments: &ConfigArguments,
|
||||
) -> Result<ExitStatus> {
|
||||
// Construct the "default" settings. These are used when no `pyproject.toml`
|
||||
// files are present, or files are injected from outside the hierarchy.
|
||||
let pyproject_config = resolve(config_arguments, None)?;
|
||||
if pyproject_config.settings.analyze.preview.is_disabled() {
|
||||
warn_user!("`ruff analyze graph` is experimental and may change without warning");
|
||||
}
|
||||
|
||||
// Write all paths relative to the current working directory.
|
||||
let root =
|
||||
SystemPathBuf::from_path_buf(CWD.clone()).expect("Expected a UTF-8 working directory");
|
||||
|
||||
// Find all Python files.
|
||||
let files = resolve_default_files(args.files, false);
|
||||
let (paths, resolver) = python_files_in_path(&files, &pyproject_config, config_arguments)?;
|
||||
|
||||
if paths.is_empty() {
|
||||
warn_user_once!("No Python files found under the given path(s)");
|
||||
return Ok(ExitStatus::Success);
|
||||
}
|
||||
|
||||
// Resolve all package roots.
|
||||
let package_roots = resolver
|
||||
.package_roots(
|
||||
&paths
|
||||
.iter()
|
||||
.flatten()
|
||||
.map(ResolvedFile::path)
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.into_iter()
|
||||
.map(|(path, package)| (path.to_path_buf(), package.map(Path::to_path_buf)))
|
||||
.collect::<FxHashMap<_, _>>();
|
||||
|
||||
// Create a database for each source root.
|
||||
let db = ModuleDb::from_src_roots(
|
||||
package_roots
|
||||
.values()
|
||||
.filter_map(|package| package.as_deref())
|
||||
.filter_map(|package| package.parent())
|
||||
.map(Path::to_path_buf)
|
||||
.filter_map(|path| SystemPathBuf::from_path_buf(path).ok()),
|
||||
)?;
|
||||
|
||||
// Collect and resolve the imports for each file.
|
||||
let result = Arc::new(std::sync::Mutex::new(Vec::new()));
|
||||
let inner_result = Arc::clone(&result);
|
||||
|
||||
rayon::scope(move |scope| {
|
||||
for resolved_file in paths {
|
||||
let Ok(resolved_file) = resolved_file else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let path = resolved_file.into_path();
|
||||
let package = path
|
||||
.parent()
|
||||
.and_then(|parent| package_roots.get(parent))
|
||||
.and_then(Clone::clone);
|
||||
|
||||
// Resolve the per-file settings.
|
||||
let settings = resolver.resolve(&path);
|
||||
let string_imports = settings.analyze.detect_string_imports;
|
||||
let include_dependencies = settings.analyze.include_dependencies.get(&path).cloned();
|
||||
|
||||
// Ignore non-Python files.
|
||||
let source_type = match settings.analyze.extension.get(&path) {
|
||||
None => match SourceType::from(&path) {
|
||||
SourceType::Python(source_type) => source_type,
|
||||
SourceType::Toml(_) => {
|
||||
debug!("Ignoring TOML file: {}", path.display());
|
||||
continue;
|
||||
}
|
||||
},
|
||||
Some(language) => PySourceType::from(language),
|
||||
};
|
||||
if matches!(source_type, PySourceType::Ipynb) {
|
||||
debug!("Ignoring Jupyter notebook: {}", path.display());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Convert to system paths.
|
||||
let Ok(package) = package.map(SystemPathBuf::from_path_buf).transpose() else {
|
||||
warn!("Failed to convert package to system path");
|
||||
continue;
|
||||
};
|
||||
let Ok(path) = SystemPathBuf::from_path_buf(path) else {
|
||||
warn!("Failed to convert path to system path");
|
||||
continue;
|
||||
};
|
||||
|
||||
let db = db.snapshot();
|
||||
let root = root.clone();
|
||||
let result = inner_result.clone();
|
||||
scope.spawn(move |_| {
|
||||
// Identify any imports via static analysis.
|
||||
let mut imports =
|
||||
ruff_graph::generate(&path, package.as_deref(), string_imports, &db)
|
||||
.unwrap_or_else(|err| {
|
||||
warn!("Failed to generate import map for {path}: {err}");
|
||||
ModuleImports::default()
|
||||
});
|
||||
|
||||
// Append any imports that were statically defined in the configuration.
|
||||
if let Some((root, globs)) = include_dependencies {
|
||||
match globwalk::GlobWalkerBuilder::from_patterns(root, &globs)
|
||||
.file_type(globwalk::FileType::FILE)
|
||||
.build()
|
||||
{
|
||||
Ok(walker) => {
|
||||
for entry in walker {
|
||||
let entry = match entry {
|
||||
Ok(entry) => entry,
|
||||
Err(err) => {
|
||||
warn!("Failed to read glob entry: {err}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let path = match SystemPathBuf::from_path_buf(entry.into_path()) {
|
||||
Ok(path) => path,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
"Failed to convert path to system path: {}",
|
||||
err.display()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
imports.insert(path);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("Failed to read glob walker: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert the path (and imports) to be relative to the working directory.
|
||||
let path = path
|
||||
.strip_prefix(&root)
|
||||
.map(SystemPath::to_path_buf)
|
||||
.unwrap_or(path);
|
||||
let imports = imports.relative_to(&root);
|
||||
|
||||
result.lock().unwrap().push((path, imports));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Collect the results.
|
||||
let imports = Arc::into_inner(result).unwrap().into_inner()?;
|
||||
|
||||
// Generate the import map.
|
||||
let import_map = match args.direction {
|
||||
Direction::Dependencies => ImportMap::from_iter(imports),
|
||||
Direction::Dependents => ImportMap::reverse(imports),
|
||||
};
|
||||
|
||||
// Print to JSON.
|
||||
println!("{}", serde_json::to_string_pretty(&import_map)?);
|
||||
|
||||
Ok(ExitStatus::Success)
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
pub(crate) mod add_noqa;
|
||||
pub(crate) mod analyze_graph;
|
||||
pub(crate) mod check;
|
||||
pub(crate) mod check_stdin;
|
||||
pub(crate) mod clean;
|
||||
|
|
|
@ -20,7 +20,9 @@ use ruff_linter::settings::types::OutputFormat;
|
|||
use ruff_linter::{fs, warn_user, warn_user_once};
|
||||
use ruff_workspace::Settings;
|
||||
|
||||
use crate::args::{Args, CheckCommand, Command, FormatCommand};
|
||||
use crate::args::{
|
||||
AnalyzeCommand, AnalyzeGraphCommand, Args, CheckCommand, Command, FormatCommand,
|
||||
};
|
||||
use crate::printer::{Flags as PrinterFlags, Printer};
|
||||
|
||||
pub mod args;
|
||||
|
@ -186,6 +188,7 @@ pub fn run(
|
|||
Command::Check(args) => check(args, global_options),
|
||||
Command::Format(args) => format(args, global_options),
|
||||
Command::Server(args) => server(args),
|
||||
Command::Analyze(AnalyzeCommand::Graph(args)) => graph_build(args, global_options),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -199,6 +202,12 @@ fn format(args: FormatCommand, global_options: GlobalConfigArgs) -> Result<ExitS
|
|||
}
|
||||
}
|
||||
|
||||
fn graph_build(args: AnalyzeGraphCommand, global_options: GlobalConfigArgs) -> Result<ExitStatus> {
|
||||
let (cli, config_arguments) = args.partition(global_options)?;
|
||||
|
||||
commands::analyze_graph::analyze_graph(cli, &config_arguments)
|
||||
}
|
||||
|
||||
fn server(args: ServerCommand) -> Result<ExitStatus> {
|
||||
let four = NonZeroUsize::new(4).unwrap();
|
||||
|
||||
|
|
262
crates/ruff/tests/analyze_graph.rs
Normal file
262
crates/ruff/tests/analyze_graph.rs
Normal file
|
@ -0,0 +1,262 @@
|
|||
//! Tests the interaction of the `analyze graph` command.
|
||||
|
||||
#![cfg(not(target_family = "wasm"))]
|
||||
|
||||
use assert_fs::prelude::*;
|
||||
use std::process::Command;
|
||||
use std::str;
|
||||
|
||||
use anyhow::Result;
|
||||
use assert_fs::fixture::ChildPath;
|
||||
use insta_cmd::{assert_cmd_snapshot, get_cargo_bin};
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn command() -> Command {
|
||||
let mut command = Command::new(get_cargo_bin("ruff"));
|
||||
command.arg("analyze");
|
||||
command.arg("graph");
|
||||
command.arg("--preview");
|
||||
command
|
||||
}
|
||||
|
||||
const INSTA_FILTERS: &[(&str, &str)] = &[
|
||||
// Rewrite Windows output to Unix output
|
||||
(r"\\", "/"),
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn dependencies() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let root = ChildPath::new(tempdir.path());
|
||||
|
||||
root.child("ruff").child("__init__.py").write_str("")?;
|
||||
root.child("ruff")
|
||||
.child("a.py")
|
||||
.write_str(indoc::indoc! {r#"
|
||||
import ruff.b
|
||||
"#})?;
|
||||
root.child("ruff")
|
||||
.child("b.py")
|
||||
.write_str(indoc::indoc! {r#"
|
||||
from ruff import c
|
||||
"#})?;
|
||||
root.child("ruff")
|
||||
.child("c.py")
|
||||
.write_str(indoc::indoc! {r#"
|
||||
from . import d
|
||||
"#})?;
|
||||
root.child("ruff")
|
||||
.child("d.py")
|
||||
.write_str(indoc::indoc! {r#"
|
||||
from .e import f
|
||||
"#})?;
|
||||
root.child("ruff")
|
||||
.child("e.py")
|
||||
.write_str(indoc::indoc! {r#"
|
||||
def f(): pass
|
||||
"#})?;
|
||||
|
||||
insta::with_settings!({
|
||||
filters => INSTA_FILTERS.to_vec(),
|
||||
}, {
|
||||
assert_cmd_snapshot!(command().current_dir(&root), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
{
|
||||
"ruff/__init__.py": [],
|
||||
"ruff/a.py": [
|
||||
"ruff/b.py"
|
||||
],
|
||||
"ruff/b.py": [
|
||||
"ruff/c.py"
|
||||
],
|
||||
"ruff/c.py": [
|
||||
"ruff/d.py"
|
||||
],
|
||||
"ruff/d.py": [
|
||||
"ruff/e.py"
|
||||
],
|
||||
"ruff/e.py": []
|
||||
}
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dependents() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
|
||||
let root = ChildPath::new(tempdir.path());
|
||||
|
||||
root.child("ruff").child("__init__.py").write_str("")?;
|
||||
root.child("ruff")
|
||||
.child("a.py")
|
||||
.write_str(indoc::indoc! {r#"
|
||||
import ruff.b
|
||||
"#})?;
|
||||
root.child("ruff")
|
||||
.child("b.py")
|
||||
.write_str(indoc::indoc! {r#"
|
||||
from ruff import c
|
||||
"#})?;
|
||||
root.child("ruff")
|
||||
.child("c.py")
|
||||
.write_str(indoc::indoc! {r#"
|
||||
from . import d
|
||||
"#})?;
|
||||
root.child("ruff")
|
||||
.child("d.py")
|
||||
.write_str(indoc::indoc! {r#"
|
||||
from .e import f
|
||||
"#})?;
|
||||
root.child("ruff")
|
||||
.child("e.py")
|
||||
.write_str(indoc::indoc! {r#"
|
||||
def f(): pass
|
||||
"#})?;
|
||||
|
||||
insta::with_settings!({
|
||||
filters => INSTA_FILTERS.to_vec(),
|
||||
}, {
|
||||
assert_cmd_snapshot!(command().arg("--direction").arg("dependents").current_dir(&root), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
{
|
||||
"ruff/__init__.py": [],
|
||||
"ruff/a.py": [],
|
||||
"ruff/b.py": [
|
||||
"ruff/a.py"
|
||||
],
|
||||
"ruff/c.py": [
|
||||
"ruff/b.py"
|
||||
],
|
||||
"ruff/d.py": [
|
||||
"ruff/c.py"
|
||||
],
|
||||
"ruff/e.py": [
|
||||
"ruff/d.py"
|
||||
]
|
||||
}
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_detection() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
|
||||
let root = ChildPath::new(tempdir.path());
|
||||
|
||||
root.child("ruff").child("__init__.py").write_str("")?;
|
||||
root.child("ruff")
|
||||
.child("a.py")
|
||||
.write_str(indoc::indoc! {r#"
|
||||
import ruff.b
|
||||
"#})?;
|
||||
root.child("ruff")
|
||||
.child("b.py")
|
||||
.write_str(indoc::indoc! {r#"
|
||||
import importlib
|
||||
|
||||
importlib.import_module("ruff.c")
|
||||
"#})?;
|
||||
root.child("ruff").child("c.py").write_str("")?;
|
||||
|
||||
insta::with_settings!({
|
||||
filters => INSTA_FILTERS.to_vec(),
|
||||
}, {
|
||||
assert_cmd_snapshot!(command().current_dir(&root), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
{
|
||||
"ruff/__init__.py": [],
|
||||
"ruff/a.py": [
|
||||
"ruff/b.py"
|
||||
],
|
||||
"ruff/b.py": [],
|
||||
"ruff/c.py": []
|
||||
}
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
});
|
||||
|
||||
insta::with_settings!({
|
||||
filters => INSTA_FILTERS.to_vec(),
|
||||
}, {
|
||||
assert_cmd_snapshot!(command().arg("--detect-string-imports").current_dir(&root), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
{
|
||||
"ruff/__init__.py": [],
|
||||
"ruff/a.py": [
|
||||
"ruff/b.py"
|
||||
],
|
||||
"ruff/b.py": [
|
||||
"ruff/c.py"
|
||||
],
|
||||
"ruff/c.py": []
|
||||
}
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn globs() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
|
||||
let root = ChildPath::new(tempdir.path());
|
||||
|
||||
root.child("ruff.toml").write_str(indoc::indoc! {r#"
|
||||
[analyze]
|
||||
include-dependencies = { "ruff/a.py" = ["ruff/b.py"], "ruff/b.py" = ["ruff/*.py"] }
|
||||
"#})?;
|
||||
|
||||
root.child("ruff").child("__init__.py").write_str("")?;
|
||||
root.child("ruff").child("a.py").write_str("")?;
|
||||
root.child("ruff").child("b.py").write_str("")?;
|
||||
root.child("ruff").child("c.py").write_str("")?;
|
||||
|
||||
insta::with_settings!({
|
||||
filters => INSTA_FILTERS.to_vec(),
|
||||
}, {
|
||||
assert_cmd_snapshot!(command().current_dir(&root), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
{
|
||||
"ruff/__init__.py": [],
|
||||
"ruff/a.py": [
|
||||
"ruff/b.py"
|
||||
],
|
||||
"ruff/b.py": [
|
||||
"ruff/__init__.py",
|
||||
"ruff/a.py",
|
||||
"ruff/b.py",
|
||||
"ruff/c.py"
|
||||
],
|
||||
"ruff/c.py": []
|
||||
}
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -200,7 +200,7 @@ linter.safety_table.forced_unsafe = []
|
|||
linter.target_version = Py37
|
||||
linter.preview = disabled
|
||||
linter.explicit_preview_rules = false
|
||||
linter.extension.mapping = {}
|
||||
linter.extension = ExtensionMapping({})
|
||||
linter.allowed_confusables = []
|
||||
linter.builtins = []
|
||||
linter.dummy_variable_rgx = ^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$
|
||||
|
@ -388,4 +388,10 @@ formatter.magic_trailing_comma = respect
|
|||
formatter.docstring_code_format = disabled
|
||||
formatter.docstring_code_line_width = dynamic
|
||||
|
||||
# Analyze Settings
|
||||
analyze.preview = disabled
|
||||
analyze.detect_string_imports = false
|
||||
analyze.extension = ExtensionMapping({})
|
||||
analyze.include_dependencies = {}
|
||||
|
||||
----- stderr -----
|
||||
|
|
|
@ -26,6 +26,7 @@ filetime = { workspace = true }
|
|||
ignore = { workspace = true, optional = true }
|
||||
matchit = { workspace = true }
|
||||
salsa = { workspace = true }
|
||||
serde = { workspace = true, optional = true }
|
||||
path-slash = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
@ -47,5 +48,6 @@ tempfile = { workspace = true }
|
|||
[features]
|
||||
cache = ["ruff_cache"]
|
||||
os = ["ignore"]
|
||||
serde = ["dep:serde", "camino/serde1"]
|
||||
# Exposes testing utilities.
|
||||
testing = ["tracing-subscriber", "tracing-tree"]
|
||||
|
|
|
@ -16,7 +16,7 @@ use super::walk_directory::{
|
|||
};
|
||||
|
||||
/// A system implementation that uses the OS file system.
|
||||
#[derive(Default, Debug)]
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct OsSystem {
|
||||
inner: Arc<OsSystemInner>,
|
||||
}
|
||||
|
|
|
@ -593,6 +593,27 @@ impl ruff_cache::CacheKey for SystemPathBuf {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl serde::Serialize for SystemPath {
|
||||
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
self.0.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl serde::Serialize for SystemPathBuf {
|
||||
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
self.0.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl<'de> serde::Deserialize<'de> for SystemPathBuf {
|
||||
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
Utf8PathBuf::deserialize(deserializer).map(SystemPathBuf)
|
||||
}
|
||||
}
|
||||
|
||||
/// A slice of a virtual path on [`System`](super::System) (akin to [`str`]).
|
||||
#[repr(transparent)]
|
||||
pub struct SystemVirtualPath(str);
|
||||
|
|
31
crates/ruff_graph/Cargo.toml
Normal file
31
crates/ruff_graph/Cargo.toml
Normal file
|
@ -0,0 +1,31 @@
|
|||
[package]
|
||||
name = "ruff_graph"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
homepage.workspace = true
|
||||
documentation.workspace = true
|
||||
repository.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
red_knot_python_semantic = { workspace = true }
|
||||
ruff_cache = { workspace = true }
|
||||
ruff_db = { workspace = true, features = ["os", "serde"] }
|
||||
ruff_linter = { workspace = true }
|
||||
ruff_macros = { workspace = true }
|
||||
ruff_python_ast = { workspace = true }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
clap = { workspace = true, optional = true }
|
||||
salsa = { workspace = true }
|
||||
schemars = { workspace = true, optional = true }
|
||||
serde = { workspace = true, optional = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[package.metadata.cargo-shear]
|
||||
# Used via `CacheKey` macro expansion.
|
||||
ignored = ["ruff_cache"]
|
111
crates/ruff_graph/src/collector.rs
Normal file
111
crates/ruff_graph/src/collector.rs
Normal file
|
@ -0,0 +1,111 @@
|
|||
use red_knot_python_semantic::ModuleName;
|
||||
use ruff_python_ast::visitor::source_order::{walk_body, walk_expr, walk_stmt, SourceOrderVisitor};
|
||||
use ruff_python_ast::{self as ast, Expr, ModModule, Stmt};
|
||||
|
||||
/// Collect all imports for a given Python file.
|
||||
#[derive(Default, Debug)]
|
||||
pub(crate) struct Collector<'a> {
|
||||
/// The path to the current module.
|
||||
module_path: Option<&'a [String]>,
|
||||
/// Whether to detect imports from string literals.
|
||||
string_imports: bool,
|
||||
/// The collected imports from the Python AST.
|
||||
imports: Vec<CollectedImport>,
|
||||
}
|
||||
|
||||
impl<'a> Collector<'a> {
|
||||
pub(crate) fn new(module_path: Option<&'a [String]>, string_imports: bool) -> Self {
|
||||
Self {
|
||||
module_path,
|
||||
string_imports,
|
||||
imports: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub(crate) fn collect(mut self, module: &ModModule) -> Vec<CollectedImport> {
|
||||
walk_body(&mut self, &module.body);
|
||||
self.imports
|
||||
}
|
||||
}
|
||||
|
||||
impl<'ast> SourceOrderVisitor<'ast> for Collector<'_> {
|
||||
fn visit_stmt(&mut self, stmt: &'ast Stmt) {
|
||||
match stmt {
|
||||
Stmt::ImportFrom(ast::StmtImportFrom {
|
||||
names,
|
||||
module,
|
||||
level,
|
||||
range: _,
|
||||
}) => {
|
||||
let module = module.as_deref();
|
||||
let level = *level;
|
||||
for alias in names {
|
||||
let mut components = vec![];
|
||||
|
||||
if level > 0 {
|
||||
// If we're resolving a relative import, we must have a module path.
|
||||
let Some(module_path) = self.module_path else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Start with the containing module.
|
||||
components.extend(module_path.iter().map(String::as_str));
|
||||
|
||||
// Remove segments based on the number of dots.
|
||||
for _ in 0..level {
|
||||
if components.is_empty() {
|
||||
return;
|
||||
}
|
||||
components.pop();
|
||||
}
|
||||
}
|
||||
|
||||
// Add the module path.
|
||||
if let Some(module) = module {
|
||||
components.extend(module.split('.'));
|
||||
}
|
||||
|
||||
// Add the alias name.
|
||||
components.push(alias.name.as_str());
|
||||
|
||||
if let Some(module_name) = ModuleName::from_components(components) {
|
||||
self.imports.push(CollectedImport::ImportFrom(module_name));
|
||||
}
|
||||
}
|
||||
}
|
||||
Stmt::Import(ast::StmtImport { names, range: _ }) => {
|
||||
for alias in names {
|
||||
if let Some(module_name) = ModuleName::new(alias.name.as_str()) {
|
||||
self.imports.push(CollectedImport::Import(module_name));
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
walk_stmt(self, stmt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_expr(&mut self, expr: &'ast Expr) {
|
||||
if self.string_imports {
|
||||
if let Expr::StringLiteral(ast::ExprStringLiteral { value, range: _ }) = expr {
|
||||
// Determine whether the string literal "looks like" an import statement: contains
|
||||
// a dot, and consists solely of valid Python identifiers.
|
||||
let value = value.to_str();
|
||||
if let Some(module_name) = ModuleName::new(value) {
|
||||
self.imports.push(CollectedImport::Import(module_name));
|
||||
}
|
||||
}
|
||||
walk_expr(self, expr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum CollectedImport {
|
||||
/// The import was part of an `import` statement.
|
||||
Import(ModuleName),
|
||||
/// The import was part of an `import from` statement.
|
||||
ImportFrom(ModuleName),
|
||||
}
|
94
crates/ruff_graph/src/db.rs
Normal file
94
crates/ruff_graph/src/db.rs
Normal file
|
@ -0,0 +1,94 @@
|
|||
use anyhow::Result;
|
||||
use red_knot_python_semantic::{Db, Program, ProgramSettings, PythonVersion, SearchPathSettings};
|
||||
use ruff_db::files::{File, Files};
|
||||
use ruff_db::system::{OsSystem, System, SystemPathBuf};
|
||||
use ruff_db::vendored::VendoredFileSystem;
|
||||
use ruff_db::{Db as SourceDb, Upcast};
|
||||
|
||||
#[salsa::db]
|
||||
#[derive(Default)]
|
||||
pub struct ModuleDb {
|
||||
storage: salsa::Storage<Self>,
|
||||
files: Files,
|
||||
system: OsSystem,
|
||||
vendored: VendoredFileSystem,
|
||||
}
|
||||
|
||||
impl ModuleDb {
|
||||
/// Initialize a [`ModuleDb`] from the given source root.
|
||||
pub fn from_src_roots(mut src_roots: impl Iterator<Item = SystemPathBuf>) -> Result<Self> {
|
||||
let search_paths = {
|
||||
// Use the first source root.
|
||||
let src_root = src_roots
|
||||
.next()
|
||||
.ok_or_else(|| anyhow::anyhow!("No source roots provided"))?;
|
||||
|
||||
let mut search_paths = SearchPathSettings::new(src_root.to_path_buf());
|
||||
|
||||
// Add the remaining source roots as extra paths.
|
||||
for src_root in src_roots {
|
||||
search_paths.extra_paths.push(src_root.to_path_buf());
|
||||
}
|
||||
|
||||
search_paths
|
||||
};
|
||||
|
||||
let db = Self::default();
|
||||
Program::from_settings(
|
||||
&db,
|
||||
&ProgramSettings {
|
||||
target_version: PythonVersion::default(),
|
||||
search_paths,
|
||||
},
|
||||
)?;
|
||||
|
||||
Ok(db)
|
||||
}
|
||||
|
||||
/// Create a snapshot of the current database.
|
||||
#[must_use]
|
||||
pub fn snapshot(&self) -> Self {
|
||||
Self {
|
||||
storage: self.storage.clone(),
|
||||
system: self.system.clone(),
|
||||
vendored: self.vendored.clone(),
|
||||
files: self.files.snapshot(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Upcast<dyn SourceDb> for ModuleDb {
|
||||
fn upcast(&self) -> &(dyn SourceDb + 'static) {
|
||||
self
|
||||
}
|
||||
fn upcast_mut(&mut self) -> &mut (dyn SourceDb + 'static) {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
impl SourceDb for ModuleDb {
|
||||
fn vendored(&self) -> &VendoredFileSystem {
|
||||
&self.vendored
|
||||
}
|
||||
|
||||
fn system(&self) -> &dyn System {
|
||||
&self.system
|
||||
}
|
||||
|
||||
fn files(&self) -> &Files {
|
||||
&self.files
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
impl Db for ModuleDb {
|
||||
fn is_file_open(&self, file: File) -> bool {
|
||||
!file.path(self).is_vendored_path()
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
impl salsa::Database for ModuleDb {
|
||||
fn salsa_event(&self, _event: &dyn Fn() -> salsa::Event) {}
|
||||
}
|
120
crates/ruff_graph/src/lib.rs
Normal file
120
crates/ruff_graph/src/lib.rs
Normal file
|
@ -0,0 +1,120 @@
|
|||
use crate::collector::Collector;
|
||||
pub use crate::db::ModuleDb;
|
||||
use crate::resolver::Resolver;
|
||||
pub use crate::settings::{AnalyzeSettings, Direction};
|
||||
use anyhow::Result;
|
||||
use red_knot_python_semantic::SemanticModel;
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_db::system::{SystemPath, SystemPathBuf};
|
||||
use ruff_python_ast::helpers::to_module_path;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
mod collector;
|
||||
mod db;
|
||||
mod resolver;
|
||||
mod settings;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub struct ModuleImports(BTreeSet<SystemPathBuf>);
|
||||
|
||||
impl ModuleImports {
|
||||
/// Insert a file path into the module imports.
|
||||
pub fn insert(&mut self, path: SystemPathBuf) {
|
||||
self.0.insert(path);
|
||||
}
|
||||
|
||||
/// Returns `true` if the module imports are empty.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
|
||||
/// Returns the number of module imports.
|
||||
pub fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
/// Convert the file paths to be relative to a given path.
|
||||
#[must_use]
|
||||
pub fn relative_to(self, path: &SystemPath) -> Self {
|
||||
Self(
|
||||
self.0
|
||||
.into_iter()
|
||||
.map(|import| {
|
||||
import
|
||||
.strip_prefix(path)
|
||||
.map(SystemPath::to_path_buf)
|
||||
.unwrap_or(import)
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub struct ImportMap(BTreeMap<SystemPathBuf, ModuleImports>);
|
||||
|
||||
impl ImportMap {
|
||||
/// Insert a module's imports into the map.
|
||||
pub fn insert(&mut self, path: SystemPathBuf, imports: ModuleImports) {
|
||||
self.0.insert(path, imports);
|
||||
}
|
||||
|
||||
/// Reverse the [`ImportMap`], e.g., to convert from dependencies to dependents.
|
||||
#[must_use]
|
||||
pub fn reverse(imports: impl IntoIterator<Item = (SystemPathBuf, ModuleImports)>) -> Self {
|
||||
let mut reverse = ImportMap::default();
|
||||
for (path, imports) in imports {
|
||||
for import in imports.0 {
|
||||
reverse.0.entry(import).or_default().insert(path.clone());
|
||||
}
|
||||
reverse.0.entry(path).or_default();
|
||||
}
|
||||
reverse
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIterator<(SystemPathBuf, ModuleImports)> for ImportMap {
|
||||
fn from_iter<I: IntoIterator<Item = (SystemPathBuf, ModuleImports)>>(iter: I) -> Self {
|
||||
let mut map = ImportMap::default();
|
||||
for (path, imports) in iter {
|
||||
map.0.entry(path).or_default().0.extend(imports.0);
|
||||
}
|
||||
map
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate the module imports for a given Python file.
|
||||
pub fn generate(
|
||||
path: &SystemPath,
|
||||
package: Option<&SystemPath>,
|
||||
string_imports: bool,
|
||||
db: &ModuleDb,
|
||||
) -> Result<ModuleImports> {
|
||||
// Read and parse the source code.
|
||||
let file = system_path_to_file(db, path)?;
|
||||
let parsed = parsed_module(db, file);
|
||||
let module_path =
|
||||
package.and_then(|package| to_module_path(package.as_std_path(), path.as_std_path()));
|
||||
let model = SemanticModel::new(db, file);
|
||||
|
||||
// Collect the imports.
|
||||
let imports = Collector::new(module_path.as_deref(), string_imports).collect(parsed.syntax());
|
||||
|
||||
// Resolve the imports.
|
||||
let mut resolved_imports = ModuleImports::default();
|
||||
for import in imports {
|
||||
let Some(resolved) = Resolver::new(&model).resolve(import) else {
|
||||
continue;
|
||||
};
|
||||
let Some(path) = resolved.as_system_path() else {
|
||||
continue;
|
||||
};
|
||||
resolved_imports.insert(path.to_path_buf());
|
||||
}
|
||||
|
||||
Ok(resolved_imports)
|
||||
}
|
39
crates/ruff_graph/src/resolver.rs
Normal file
39
crates/ruff_graph/src/resolver.rs
Normal file
|
@ -0,0 +1,39 @@
|
|||
use red_knot_python_semantic::SemanticModel;
|
||||
use ruff_db::files::FilePath;
|
||||
|
||||
use crate::collector::CollectedImport;
|
||||
|
||||
/// Collect all imports for a given Python file.
|
||||
pub(crate) struct Resolver<'a> {
|
||||
semantic: &'a SemanticModel<'a>,
|
||||
}
|
||||
|
||||
impl<'a> Resolver<'a> {
|
||||
/// Initialize a [`Resolver`] with a given [`SemanticModel`].
|
||||
pub(crate) fn new(semantic: &'a SemanticModel<'a>) -> Self {
|
||||
Self { semantic }
|
||||
}
|
||||
|
||||
/// Resolve the [`CollectedImport`] into a [`FilePath`].
|
||||
pub(crate) fn resolve(&self, import: CollectedImport) -> Option<&'a FilePath> {
|
||||
match import {
|
||||
CollectedImport::Import(import) => self
|
||||
.semantic
|
||||
.resolve_module(import)
|
||||
.map(|module| module.file().path(self.semantic.db())),
|
||||
CollectedImport::ImportFrom(import) => {
|
||||
// Attempt to resolve the member (e.g., given `from foo import bar`, look for `foo.bar`).
|
||||
let parent = import.parent();
|
||||
self.semantic
|
||||
.resolve_module(import)
|
||||
.map(|module| module.file().path(self.semantic.db()))
|
||||
.or_else(|| {
|
||||
// Attempt to resolve the module (e.g., given `from foo import bar`, look for `foo`).
|
||||
self.semantic
|
||||
.resolve_module(parent?)
|
||||
.map(|module| module.file().path(self.semantic.db()))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
52
crates/ruff_graph/src/settings.rs
Normal file
52
crates/ruff_graph/src/settings.rs
Normal file
|
@ -0,0 +1,52 @@
|
|||
use ruff_linter::display_settings;
|
||||
use ruff_linter::settings::types::{ExtensionMapping, PreviewMode};
|
||||
use ruff_macros::CacheKey;
|
||||
use std::collections::BTreeMap;
|
||||
use std::fmt;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Default, Clone, CacheKey)]
|
||||
pub struct AnalyzeSettings {
|
||||
pub preview: PreviewMode,
|
||||
pub detect_string_imports: bool,
|
||||
pub include_dependencies: BTreeMap<PathBuf, (PathBuf, Vec<String>)>,
|
||||
pub extension: ExtensionMapping,
|
||||
}
|
||||
|
||||
impl fmt::Display for AnalyzeSettings {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
writeln!(f, "\n# Analyze Settings")?;
|
||||
display_settings! {
|
||||
formatter = f,
|
||||
namespace = "analyze",
|
||||
fields = [
|
||||
self.preview,
|
||||
self.detect_string_imports,
|
||||
self.extension | debug,
|
||||
self.include_dependencies | debug,
|
||||
]
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, CacheKey)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
|
||||
pub enum Direction {
|
||||
/// Construct a map from module to its dependencies (i.e., the modules that it imports).
|
||||
#[default]
|
||||
Dependencies,
|
||||
/// Construct a map from module to its dependents (i.e., the modules that import it).
|
||||
Dependents,
|
||||
}
|
||||
|
||||
impl fmt::Display for Direction {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Dependencies => write!(f, "\"dependencies\""),
|
||||
Self::Dependents => write!(f, "\"dependents\""),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -152,6 +152,8 @@ pub fn set_up_logging(level: LogLevel) -> Result<()> {
|
|||
})
|
||||
.level(level.level_filter())
|
||||
.level_for("globset", log::LevelFilter::Warn)
|
||||
.level_for("red_knot_python_semantic", log::LevelFilter::Warn)
|
||||
.level_for("salsa", log::LevelFilter::Warn)
|
||||
.chain(std::io::stderr())
|
||||
.apply()?;
|
||||
Ok(())
|
||||
|
|
|
@ -285,7 +285,7 @@ impl Display for LinterSettings {
|
|||
self.target_version | debug,
|
||||
self.preview,
|
||||
self.explicit_preview_rules,
|
||||
self.extension | nested,
|
||||
self.extension | debug,
|
||||
|
||||
self.allowed_confusables | array,
|
||||
self.builtins | array,
|
||||
|
|
|
@ -478,46 +478,31 @@ impl From<ExtensionPair> for (String, Language) {
|
|||
(value.extension, value.language)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, CacheKey)]
|
||||
pub struct ExtensionMapping {
|
||||
mapping: FxHashMap<String, Language>,
|
||||
}
|
||||
pub struct ExtensionMapping(FxHashMap<String, Language>);
|
||||
|
||||
impl ExtensionMapping {
|
||||
/// Return the [`Language`] for the given file.
|
||||
pub fn get(&self, path: &Path) -> Option<Language> {
|
||||
let ext = path.extension()?.to_str()?;
|
||||
self.mapping.get(ext).copied()
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ExtensionMapping {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
display_settings! {
|
||||
formatter = f,
|
||||
namespace = "linter.extension",
|
||||
fields = [
|
||||
self.mapping | debug
|
||||
]
|
||||
}
|
||||
Ok(())
|
||||
self.0.get(ext).copied()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FxHashMap<String, Language>> for ExtensionMapping {
|
||||
fn from(value: FxHashMap<String, Language>) -> Self {
|
||||
Self { mapping: value }
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIterator<ExtensionPair> for ExtensionMapping {
|
||||
fn from_iter<T: IntoIterator<Item = ExtensionPair>>(iter: T) -> Self {
|
||||
Self {
|
||||
mapping: iter
|
||||
.into_iter()
|
||||
Self(
|
||||
iter.into_iter()
|
||||
.map(|pair| (pair.extension, pair.language))
|
||||
.collect(),
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -13,14 +13,15 @@ license = { workspace = true }
|
|||
[lib]
|
||||
|
||||
[dependencies]
|
||||
ruff_linter = { workspace = true }
|
||||
ruff_cache = { workspace = true }
|
||||
ruff_formatter = { workspace = true }
|
||||
ruff_python_formatter = { workspace = true, features = ["serde"] }
|
||||
ruff_graph = { workspace = true, features = ["serde", "schemars"] }
|
||||
ruff_linter = { workspace = true }
|
||||
ruff_macros = { workspace = true }
|
||||
ruff_python_ast = { workspace = true }
|
||||
ruff_python_formatter = { workspace = true, features = ["serde"] }
|
||||
ruff_python_semantic = { workspace = true, features = ["serde"] }
|
||||
ruff_source_file = { workspace = true }
|
||||
ruff_cache = { workspace = true }
|
||||
ruff_macros = { workspace = true }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
colored = { workspace = true }
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
//! the various parameters.
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::collections::BTreeMap;
|
||||
use std::env::VarError;
|
||||
use std::num::{NonZeroU16, NonZeroU8};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
@ -19,6 +20,7 @@ use strum::IntoEnumIterator;
|
|||
|
||||
use ruff_cache::cache_dir;
|
||||
use ruff_formatter::IndentStyle;
|
||||
use ruff_graph::{AnalyzeSettings, Direction};
|
||||
use ruff_linter::line_width::{IndentWidth, LineLength};
|
||||
use ruff_linter::registry::RuleNamespace;
|
||||
use ruff_linter::registry::{Rule, RuleSet, INCOMPATIBLE_CODES};
|
||||
|
@ -40,11 +42,11 @@ use ruff_python_formatter::{
|
|||
};
|
||||
|
||||
use crate::options::{
|
||||
Flake8AnnotationsOptions, Flake8BanditOptions, Flake8BooleanTrapOptions, Flake8BugbearOptions,
|
||||
Flake8BuiltinsOptions, Flake8ComprehensionsOptions, Flake8CopyrightOptions,
|
||||
Flake8ErrMsgOptions, Flake8GetTextOptions, Flake8ImplicitStrConcatOptions,
|
||||
Flake8ImportConventionsOptions, Flake8PytestStyleOptions, Flake8QuotesOptions,
|
||||
Flake8SelfOptions, Flake8TidyImportsOptions, Flake8TypeCheckingOptions,
|
||||
AnalyzeOptions, Flake8AnnotationsOptions, Flake8BanditOptions, Flake8BooleanTrapOptions,
|
||||
Flake8BugbearOptions, Flake8BuiltinsOptions, Flake8ComprehensionsOptions,
|
||||
Flake8CopyrightOptions, Flake8ErrMsgOptions, Flake8GetTextOptions,
|
||||
Flake8ImplicitStrConcatOptions, Flake8ImportConventionsOptions, Flake8PytestStyleOptions,
|
||||
Flake8QuotesOptions, Flake8SelfOptions, Flake8TidyImportsOptions, Flake8TypeCheckingOptions,
|
||||
Flake8UnusedArgumentsOptions, FormatOptions, IsortOptions, LintCommonOptions, LintOptions,
|
||||
McCabeOptions, Options, Pep8NamingOptions, PyUpgradeOptions, PycodestyleOptions,
|
||||
PydocstyleOptions, PyflakesOptions, PylintOptions, RuffOptions,
|
||||
|
@ -142,6 +144,7 @@ pub struct Configuration {
|
|||
|
||||
pub lint: LintConfiguration,
|
||||
pub format: FormatConfiguration,
|
||||
pub analyze: AnalyzeConfiguration,
|
||||
}
|
||||
|
||||
impl Configuration {
|
||||
|
@ -207,6 +210,21 @@ impl Configuration {
|
|||
.unwrap_or(format_defaults.docstring_code_line_width),
|
||||
};
|
||||
|
||||
let analyze = self.analyze;
|
||||
let analyze_preview = analyze.preview.unwrap_or(global_preview);
|
||||
let analyze_defaults = AnalyzeSettings::default();
|
||||
|
||||
let analyze = AnalyzeSettings {
|
||||
preview: analyze_preview,
|
||||
extension: self.extension.clone().unwrap_or_default(),
|
||||
detect_string_imports: analyze
|
||||
.detect_string_imports
|
||||
.unwrap_or(analyze_defaults.detect_string_imports),
|
||||
include_dependencies: analyze
|
||||
.include_dependencies
|
||||
.unwrap_or(analyze_defaults.include_dependencies),
|
||||
};
|
||||
|
||||
let lint = self.lint;
|
||||
let lint_preview = lint.preview.unwrap_or(global_preview);
|
||||
|
||||
|
@ -401,6 +419,7 @@ impl Configuration {
|
|||
},
|
||||
|
||||
formatter,
|
||||
analyze,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -534,6 +553,10 @@ impl Configuration {
|
|||
options.format.unwrap_or_default(),
|
||||
project_root,
|
||||
)?,
|
||||
analyze: AnalyzeConfiguration::from_options(
|
||||
options.analyze.unwrap_or_default(),
|
||||
project_root,
|
||||
)?,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -573,6 +596,7 @@ impl Configuration {
|
|||
|
||||
lint: self.lint.combine(config.lint),
|
||||
format: self.format.combine(config.format),
|
||||
analyze: self.analyze.combine(config.analyze),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1191,6 +1215,45 @@ impl FormatConfiguration {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct AnalyzeConfiguration {
|
||||
pub preview: Option<PreviewMode>,
|
||||
pub direction: Option<Direction>,
|
||||
pub detect_string_imports: Option<bool>,
|
||||
pub include_dependencies: Option<BTreeMap<PathBuf, (PathBuf, Vec<String>)>>,
|
||||
}
|
||||
|
||||
impl AnalyzeConfiguration {
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub fn from_options(options: AnalyzeOptions, project_root: &Path) -> Result<Self> {
|
||||
Ok(Self {
|
||||
preview: options.preview.map(PreviewMode::from),
|
||||
direction: options.direction,
|
||||
detect_string_imports: options.detect_string_imports,
|
||||
include_dependencies: options.include_dependencies.map(|dependencies| {
|
||||
dependencies
|
||||
.into_iter()
|
||||
.map(|(key, value)| {
|
||||
(project_root.join(key), (project_root.to_path_buf(), value))
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>()
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub fn combine(self, config: Self) -> Self {
|
||||
Self {
|
||||
preview: self.preview.or(config.preview),
|
||||
direction: self.direction.or(config.direction),
|
||||
detect_string_imports: self.detect_string_imports.or(config.detect_string_imports),
|
||||
include_dependencies: self.include_dependencies.or(config.include_dependencies),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait CombinePluginOptions {
|
||||
#[must_use]
|
||||
fn combine(self, other: Self) -> Self;
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
use regex::Regex;
|
||||
use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
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;
|
||||
use ruff_linter::line_width::{IndentWidth, LineLength};
|
||||
use ruff_linter::rules::flake8_import_conventions::settings::BannedAliases;
|
||||
use ruff_linter::rules::flake8_pytest_style::settings::SettingsError;
|
||||
|
@ -433,6 +436,10 @@ pub struct Options {
|
|||
/// Options to configure code formatting.
|
||||
#[option_group]
|
||||
pub format: Option<FormatOptions>,
|
||||
|
||||
/// Options to configure import map generation.
|
||||
#[option_group]
|
||||
pub analyze: Option<AnalyzeOptions>,
|
||||
}
|
||||
|
||||
/// Configures how Ruff checks your code.
|
||||
|
@ -3306,6 +3313,59 @@ pub struct FormatOptions {
|
|||
pub docstring_code_line_length: Option<DocstringCodeLineWidth>,
|
||||
}
|
||||
|
||||
/// Configures Ruff's `analyze` command.
|
||||
#[derive(
|
||||
Clone, Debug, PartialEq, Eq, Default, Deserialize, Serialize, OptionsMetadata, CombineOptions,
|
||||
)]
|
||||
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct AnalyzeOptions {
|
||||
/// Whether to enable preview mode. When preview mode is enabled, Ruff will expose unstable
|
||||
/// commands.
|
||||
#[option(
|
||||
default = "false",
|
||||
value_type = "bool",
|
||||
example = r#"
|
||||
# Enable preview features.
|
||||
preview = true
|
||||
"#
|
||||
)]
|
||||
pub preview: Option<bool>,
|
||||
/// Whether to generate a map from file to files that it depends on (dependencies) or files that
|
||||
/// depend on it (dependents).
|
||||
#[option(
|
||||
default = r#"\"dependencies\""#,
|
||||
value_type = "\"dependents\" | \"dependencies\"",
|
||||
example = r#"
|
||||
direction = "dependencies"
|
||||
"#
|
||||
)]
|
||||
pub direction: Option<Direction>,
|
||||
/// Whether to detect imports from string literals. When enabled, Ruff will search for string
|
||||
/// literals that "look like" import paths, and include them in the import map, if they resolve
|
||||
/// to valid Python modules.
|
||||
#[option(
|
||||
default = "false",
|
||||
value_type = "bool",
|
||||
example = r#"
|
||||
detect-string-imports = true
|
||||
"#
|
||||
)]
|
||||
pub detect_string_imports: Option<bool>,
|
||||
/// A map from file path to the list of file paths or globs that should be considered
|
||||
/// dependencies of that file, regardless of whether relevant imports are detected.
|
||||
#[option(
|
||||
default = "{}",
|
||||
value_type = "dict[str, list[str]]",
|
||||
example = r#"
|
||||
include-dependencies = {
|
||||
"foo/bar.py": ["foo/baz/*.py"],
|
||||
}
|
||||
"#
|
||||
)]
|
||||
pub include_dependencies: Option<BTreeMap<PathBuf, Vec<String>>>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::options::Flake8SelfOptions;
|
||||
|
|
|
@ -395,7 +395,6 @@ pub fn python_files_in_path<'a>(
|
|||
let walker = builder.build_parallel();
|
||||
|
||||
// Run the `WalkParallel` to collect all Python files.
|
||||
|
||||
let state = WalkPythonFilesState::new(resolver);
|
||||
let mut visitor = PythonFilesVisitorBuilder::new(transformer, &state);
|
||||
walker.visit(&mut visitor);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
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,
|
||||
|
@ -35,6 +36,7 @@ pub struct Settings {
|
|||
pub file_resolver: FileResolverSettings,
|
||||
pub linter: LinterSettings,
|
||||
pub formatter: FormatterSettings,
|
||||
pub analyze: AnalyzeSettings,
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
|
@ -50,6 +52,7 @@ impl Default for Settings {
|
|||
linter: LinterSettings::new(project_root),
|
||||
file_resolver: FileResolverSettings::new(project_root),
|
||||
formatter: FormatterSettings::default(),
|
||||
analyze: AnalyzeSettings::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -68,7 +71,8 @@ impl fmt::Display for Settings {
|
|||
self.unsafe_fixes,
|
||||
self.file_resolver | nested,
|
||||
self.linter | nested,
|
||||
self.formatter | nested
|
||||
self.formatter | nested,
|
||||
self.analyze | nested,
|
||||
]
|
||||
}
|
||||
Ok(())
|
||||
|
|
|
@ -522,6 +522,7 @@ Commands:
|
|||
clean Clear any caches in the current directory and any subdirectories
|
||||
format Run the Ruff formatter on the given files or directories
|
||||
server Run the language server
|
||||
analyze Run analysis over Python source code
|
||||
version Display Ruff's version
|
||||
help Print this message or the help of the given subcommand(s)
|
||||
|
||||
|
|
74
ruff.schema.json
generated
74
ruff.schema.json
generated
|
@ -16,6 +16,17 @@
|
|||
"minLength": 1
|
||||
}
|
||||
},
|
||||
"analyze": {
|
||||
"description": "Options to configure import map generation.",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AnalyzeOptions"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"builtins": {
|
||||
"description": "A list of builtins to treat as defined references, in addition to the system builtins.",
|
||||
"type": [
|
||||
|
@ -746,6 +757,51 @@
|
|||
},
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"AnalyzeOptions": {
|
||||
"description": "Configures Ruff's `analyze` command.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"detect-string-imports": {
|
||||
"description": "Whether to detect imports from string literals. When enabled, Ruff will search for string literals that \"look like\" import paths, and include them in the import map, if they resolve to valid Python modules.",
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"direction": {
|
||||
"description": "Whether to generate a map from file to files that it depends on (dependencies) or files that depend on it (dependents).",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Direction"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"include-dependencies": {
|
||||
"description": "A map from file path to the list of file paths or globs that should be considered dependencies of that file, regardless of whether relevant imports are detected.",
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"preview": {
|
||||
"description": "Whether to enable preview mode. When preview mode is enabled, Ruff will expose unstable commands.",
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"ApiBan": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
@ -800,6 +856,24 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"Direction": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Construct a map from module to its dependencies (i.e., the modules that it imports).",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Dependencies"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Construct a map from module to its dependents (i.e., the modules that import it).",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Dependents"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"DocstringCodeLineWidth": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue