mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 13:25:17 +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
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)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue