Skip to content

Commit

Permalink
Gate behind preview; rename to analyze
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh committed Sep 19, 2024
1 parent 94af7e2 commit 2de42c5
Show file tree
Hide file tree
Showing 13 changed files with 201 additions and 169 deletions.
29 changes: 18 additions & 11 deletions crates/ruff/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,9 @@ pub enum Command {
Format(FormatCommand),
/// Run the language server.
Server(ServerCommand),
/// Analyze the import graph of the given files or directories.
/// Run analysis over Python source code.
#[clap(subcommand)]
Graph(GraphCommand),
Analyze(AnalyzeCommand),
/// Display Ruff's version
Version {
#[arg(long, value_enum, default_value = "text")]
Expand All @@ -143,13 +143,13 @@ pub enum Command {
}

#[derive(Debug, Subcommand)]
pub enum GraphCommand {
/// Generate a map of Python file dependencies.
Build(GraphBuildCommand),
pub enum AnalyzeCommand {
/// Generate a map of Python file dependencies or dependents.
Graph(AnalyzeGraphCommand),
}

#[derive(Clone, Debug, clap::Parser)]
pub struct GraphBuildCommand {
pub struct AnalyzeGraphCommand {
/// List of files or directories to include.
#[clap(help = "List of files or directories to include [default: .]")]
pub files: Vec<PathBuf>,
Expand All @@ -161,6 +161,11 @@ pub struct GraphBuildCommand {
/// 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
Expand Down Expand Up @@ -765,14 +770,14 @@ impl FormatCommand {
}
}

impl GraphBuildCommand {
impl AnalyzeGraphCommand {
/// Partition the CLI into command-line arguments and configuration
/// overrides.
pub fn partition(
self,
global_options: GlobalConfigArgs,
) -> anyhow::Result<(GraphArgs, ConfigArguments)> {
let format_arguments = GraphArgs {
) -> anyhow::Result<(AnalyzeGraphArgs, ConfigArguments)> {
let format_arguments = AnalyzeGraphArgs {
files: self.files,
direction: self.direction,
};
Expand All @@ -783,6 +788,7 @@ impl GraphBuildCommand {
} else {
None
},
preview: resolve_bool_arg(self.preview, self.no_preview).map(PreviewMode::from),
..ExplicitConfigOverrides::default()
};

Expand Down Expand Up @@ -1206,7 +1212,8 @@ impl LineColumnParseError {
}

/// CLI settings that are distinct from configuration (commands, lists of files, etc.).
pub struct GraphArgs {
#[derive(Clone, Debug)]
pub struct AnalyzeGraphArgs {
pub files: Vec<PathBuf>,
pub direction: Direction,
}
Expand Down Expand Up @@ -1328,7 +1335,7 @@ impl ConfigurationTransformer for ExplicitConfigOverrides {
config.extension = Some(extension.iter().cloned().collect());
}
if let Some(detect_string_imports) = &self.detect_string_imports {
config.graph.detect_string_imports = Some(*detect_string_imports);
config.analyze.detect_string_imports = Some(*detect_string_imports);
}

config
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
use crate::args::{ConfigArguments, GraphArgs};
use crate::args::{AnalyzeGraphArgs, ConfigArguments};
use crate::resolve::resolve;
use crate::{resolve_default_files, ExitStatus};
use anyhow::Result;
use log::{debug, warn};
use ruff_db::system::{SystemPath, SystemPathBuf};
use ruff_db::system::SystemPathBuf;
use ruff_graph::{Direction, ImportMap, ModuleDb, ModuleImports};
use ruff_linter::warn_user_once;
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;
Expand All @@ -14,10 +14,16 @@ use std::path::Path;
use std::sync::Arc;

/// Generate an import map.
pub(crate) fn graph(args: GraphArgs, config_arguments: &ConfigArguments) -> Result<ExitStatus> {
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");
}

// Find all Python files.
let files = resolve_default_files(args.files, false);
Expand Down Expand Up @@ -79,11 +85,11 @@ pub(crate) fn graph(args: GraphArgs, config_arguments: &ConfigArguments) -> Resu

// Resolve the per-file settings.
let settings = resolver.resolve(&path);
let string_imports = settings.graph.detect_string_imports;
let include_dependencies = settings.graph.include_dependencies.get(&path).cloned();
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.graph.extension.get(&path) {
let source_type = match settings.analyze.extension.get(&path) {
None => match SourceType::from(&path) {
SourceType::Python(source_type) => source_type,
SourceType::Toml(_) => {
Expand Down
2 changes: 1 addition & 1 deletion crates/ruff/src/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
pub(crate) mod add_noqa;
pub(crate) mod analyze_graph;
pub(crate) mod check;
pub(crate) mod check_stdin;
pub(crate) mod clean;
pub(crate) mod config;
pub(crate) mod format;
pub(crate) mod format_stdin;
pub(crate) mod graph;
pub(crate) mod linter;
pub(crate) mod rule;
pub(crate) mod server;
Expand Down
10 changes: 6 additions & 4 deletions crates/ruff/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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, GraphBuildCommand, GraphCommand};
use crate::args::{
AnalyzeCommand, AnalyzeGraphCommand, Args, CheckCommand, Command, FormatCommand,
};
use crate::printer::{Flags as PrinterFlags, Printer};

pub mod args;
Expand Down Expand Up @@ -186,7 +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::Graph(GraphCommand::Build(args)) => graph_build(args, global_options),
Command::Analyze(AnalyzeCommand::Graph(args)) => graph_build(args, global_options),
}
}

Expand All @@ -200,10 +202,10 @@ fn format(args: FormatCommand, global_options: GlobalConfigArgs) -> Result<ExitS
}
}

fn graph_build(args: GraphBuildCommand, global_options: GlobalConfigArgs) -> Result<ExitStatus> {
fn graph_build(args: AnalyzeGraphCommand, global_options: GlobalConfigArgs) -> Result<ExitStatus> {
let (cli, config_arguments) = args.partition(global_options)?;

commands::graph::graph(cli, &config_arguments)
commands::analyze_graph::analyze_graph(cli, &config_arguments)
}

fn server(args: ServerCommand) -> Result<ExitStatus> {
Expand Down
58 changes: 46 additions & 12 deletions crates/ruff_graph/src/collector.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
use red_knot_python_semantic::ModuleName;
use ruff_python_ast::helpers::collect_import_from_member;
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 {
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 Collector {
pub(crate) fn new(string_imports: bool) -> Self {
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(),
}
Expand All @@ -27,7 +29,7 @@ impl Collector {
}
}

impl<'ast> SourceOrderVisitor<'ast> for Collector {
impl<'ast> SourceOrderVisitor<'ast> for Collector<'_> {
fn visit_stmt(&mut self, stmt: &'ast Stmt) {
match stmt {
Stmt::ImportFrom(ast::StmtImportFrom {
Expand All @@ -39,13 +41,45 @@ impl<'ast> SourceOrderVisitor<'ast> for Collector {
let module = module.as_deref();
let level = *level;
for alias in names {
if let Some(module_name) = ModuleName::from_components(
collect_import_from_member(level, module, &alias.name)
.segments()
.iter()
.cloned(),
) {
self.imports.push(CollectedImport::ImportFrom(module_name));
if level == 0 {
let mut components = vec![];

// 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));
}
} else if let Some(module_path) = self.module_path {
let mut components = vec![];

// 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 name.
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));
}
}
}
}
Expand Down
7 changes: 4 additions & 3 deletions crates/ruff_graph/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::collector::Collector;
pub use crate::db::ModuleDb;
use crate::resolver::Resolver;
pub use crate::settings::{Direction, GraphSettings};
pub use crate::settings::{AnalyzeSettings, Direction};
use anyhow::Result;
use red_knot_python_semantic::SemanticModel;
use ruff_db::files::system_path_to_file;
Expand Down Expand Up @@ -62,6 +62,7 @@ impl ImportMap {
for import in imports.0 {
reverse.0.entry(import).or_default().insert(path.clone());
}
reverse.0.entry(path).or_default();
}
reverse
}
Expand Down Expand Up @@ -92,12 +93,12 @@ pub fn generate(
let model = SemanticModel::new(db, file);

// Collect the imports.
let imports = Collector::new(string_imports).collect(parsed.syntax());
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, module_path.as_deref()).resolve(import) else {
let Some(resolved) = Resolver::new(&model).resolve(import) else {
continue;
};
let Some(path) = resolved.as_system_path() else {
Expand Down
49 changes: 3 additions & 46 deletions crates/ruff_graph/src/resolver.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
use red_knot_python_semantic::{ModuleName, SemanticModel};
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>,
module_path: Option<&'a [String]>,
}

impl<'a> Resolver<'a> {
pub(crate) fn new(semantic: &'a SemanticModel<'a>, module_path: Option<&'a [String]>) -> Self {
Self {
semantic,
module_path,
}
pub(crate) fn new(semantic: &'a SemanticModel<'a>) -> Self {
Self { semantic }
}

pub(crate) fn resolve(&self, import: CollectedImport) -> Option<&'a FilePath> {
Expand All @@ -24,17 +20,6 @@ impl<'a> Resolver<'a> {
.resolve_module(import)
.map(|module| module.file().path(self.semantic.db())),
CollectedImport::ImportFrom(import) => {
// If the import is relative, resolve it relative to the current module.
let import = if import
.components()
.next()
.is_some_and(|segment| segment == ".")
{
from_relative_import(self.module_path?, import)?
} else {
import
};

// Attempt to resolve the member (e.g., given `from foo import bar`, look for `foo.bar`).
let parent = import.parent();
self.semantic
Expand All @@ -50,31 +35,3 @@ impl<'a> Resolver<'a> {
}
}
}

/// Format the call path for a relative import, or `None` if the relative import extends beyond
/// the root module.
fn from_relative_import(
// The path from which the import is relative.
module: &[String],
// The path of the import itself (e.g., given `from ..foo import bar`, `[".", ".", "foo", "bar]`).
import: ModuleName,
) -> Option<ModuleName> {
let mut components = Vec::with_capacity(module.len() + import.components().count());

// Start with the module path.
components.extend(module.iter().map(String::as_str));

// Remove segments based on the number of dots.
for segment in import.components() {
if segment == "." {
if components.is_empty() {
return None;
}
components.pop();
} else {
components.push(segment);
}
}

ModuleName::from_components(components)
}
Loading

0 comments on commit 2de42c5

Please sign in to comment.