Skip to content

Commit

Permalink
Add an import indexer
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh committed Mar 28, 2023
1 parent aed248b commit 808cb2d
Show file tree
Hide file tree
Showing 18 changed files with 713 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,8 @@ def main():
quit(1)


sys.exit(2)
def main():
sys = 1

exit(1)
quit(1)
63 changes: 63 additions & 0 deletions crates/ruff/src/autofix/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@ use rustpython_parser::ast::{ExcepthandlerKind, Expr, Keyword, Location, Stmt, S
use rustpython_parser::{lexer, Mode, Tok};

use ruff_diagnostics::Edit;
use ruff_python_ast::context::Context;
use ruff_python_ast::helpers;
use ruff_python_ast::helpers::to_absolute;
use ruff_python_ast::newlines::NewlineWithTrailingNewline;
use ruff_python_ast::source_code::{Indexer, Locator, Stylist};

use crate::cst::helpers::compose_module_path;
use crate::cst::matchers::match_module;
use crate::imports::importer::Importer;
use crate::imports::{AnyImport, Import};

/// Determine if a body contains only a single statement, taking into account
/// deleted.
Expand Down Expand Up @@ -444,6 +447,66 @@ pub fn remove_argument(
}
}

/// Generate an [`Edit`] to reference the given symbol. Returns the [`Edit`] necessary to make the
/// symbol available in the current scope along with the bound name of the symbol.
///
/// For example, assuming `module` is `"functools"` and `member` is `"lru_cache"`, this function
/// could return an [`Edit`] to add `import functools` to the top of the file, alongside with the
/// name on which the `lru_cache` symbol would be made available (`"functools.lru_cache"`).
///
/// Attempts to reuse existing imports when possible.
pub fn get_or_import_symbol(
module: &str,
member: &str,
context: &Context,
importer: &Importer,
locator: &Locator,
) -> Result<(Edit, String)> {
if let Some((source, binding)) = context.resolve_binding(module, member) {
// If the symbol is already available in the current scope, use it, and add a no-nop edit to
// force conflicts with any other fixes that might try to remove the import.
let import_edit = Edit::replacement(
locator.slice(source).to_string(),
source.location,
source.end_location.unwrap(),
);
Ok((import_edit, binding))
} else {
if let Some(stmt) = importer.get_import_from(module) {
// Case 1: `from functools import lru_cache` is in scope, and we're trying to reference
// `functools.cache`; thus, we add `cache` to the import, and return `"cache"` as the
// bound name.
if context
.find_binding(member)
.map_or(true, |binding| binding.kind.is_builtin())
{
let import_edit = importer.add_member(stmt, member)?;
Ok((import_edit, member.to_string()))
} else {
bail!(
"Unable to insert `{}` into scope due to name conflict",
member
)
}
} else {
// Case 2: No `functools` import is in scope; thus, we add `import functools`, and
// return `"functools.lru_cache"` as the bound name.
if context
.find_binding(module)
.map_or(true, |binding| binding.kind.is_builtin())
{
let import_edit = importer.add_import(&AnyImport::Import(Import::module(module)));
Ok((import_edit, format!("{module}.{member}")))
} else {
bail!(
"Unable to insert `{}` into scope due to name conflict",
module
)
}
}
}
}

#[cfg(test)]
mod tests {
use anyhow::Result;
Expand Down
19 changes: 19 additions & 0 deletions crates/ruff/src/checkers/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ use crate::docstrings::definition::{
transition_scope, Definition, DefinitionKind, Docstring, Documentable,
};
use crate::fs::relativize_path;
use crate::imports::importer::Importer;
use crate::registry::{AsRule, Rule};
use crate::rules::{
flake8_2020, flake8_annotations, flake8_bandit, flake8_blind_except, flake8_boolean_trap,
Expand Down Expand Up @@ -69,6 +70,7 @@ pub struct Checker<'a> {
pub locator: &'a Locator<'a>,
pub stylist: &'a Stylist<'a>,
pub indexer: &'a Indexer,
pub importer: Importer<'a>,
// Stateful fields.
pub ctx: Context<'a>,
pub deferred: Deferred<'a>,
Expand All @@ -91,6 +93,7 @@ impl<'a> Checker<'a> {
locator: &'a Locator,
style: &'a Stylist,
indexer: &'a Indexer,
importer: Importer<'a>,
) -> Checker<'a> {
Checker {
settings,
Expand All @@ -104,6 +107,7 @@ impl<'a> Checker<'a> {
locator,
stylist: style,
indexer,
importer,
ctx: Context::new(&settings.typing_modules, path, module_path),
deferred: Deferred::default(),
diagnostics: vec![],
Expand Down Expand Up @@ -188,6 +192,20 @@ where
}
}
}

// Track each top-level import, to guide import insertions.
if matches!(
&stmt.node,
StmtKind::Import { .. } | StmtKind::ImportFrom { .. }
) {
let scope_index = self.ctx.scope_id();
if scope_index.is_global() {
if self.ctx.current_stmt_parent().is_none() {
self.importer.visit_import(stmt);
}
}
}

// Pre-visit.
match &stmt.node {
StmtKind::Global { names } => {
Expand Down Expand Up @@ -5387,6 +5405,7 @@ pub fn check_ast(
locator,
stylist,
indexer,
Importer::new(python_ast, locator, stylist),
);
checker.bind_builtins();

Expand Down
14 changes: 12 additions & 2 deletions crates/ruff/src/cst/matchers.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use anyhow::{bail, Result};
use libcst_native::{
Attribute, Call, Comparison, Dict, Expr, Expression, Import, ImportFrom, Module, SimpleString,
SmallStatement, Statement,
Attribute, Call, Comparison, Dict, Expr, Expression, Import, ImportAlias, ImportFrom,
ImportNames, Module, SimpleString, SmallStatement, Statement,
};

pub fn match_module(module_text: &str) -> Result<Module> {
Expand Down Expand Up @@ -54,6 +54,16 @@ pub fn match_import_from<'a, 'b>(module: &'a mut Module<'b>) -> Result<&'a mut I
}
}

pub fn match_aliases<'a, 'b>(
import_from: &'a mut ImportFrom<'b>,
) -> Result<&'a mut Vec<ImportAlias<'b>>> {
if let ImportNames::Aliases(aliases) = &mut import_from.names {
Ok(aliases)
} else {
bail!("Expected ImportNames::Aliases")
}
}

pub fn match_call<'a, 'b>(expression: &'a mut Expression<'b>) -> Result<&'a mut Call<'b>> {
if let Expression::Call(call) = expression {
Ok(call)
Expand Down
217 changes: 217 additions & 0 deletions crates/ruff/src/imports/importer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
//! Add and modify import statements to make symbols available during code generation.

use anyhow::Result;
use libcst_native::{Codegen, CodegenState, ImportAlias, Name, NameOrAttribute};
use rustc_hash::FxHashMap;
use rustpython_parser::ast::{Location, Stmt, StmtKind, Suite};
use rustpython_parser::{lexer, Mode, Tok};

use ruff_diagnostics::Edit;
use ruff_python_ast::helpers::is_docstring_stmt;
use ruff_python_ast::source_code::{Locator, Stylist};

use crate::cst::matchers::{match_aliases, match_import_from, match_module};
use crate::imports::AnyImport;

pub struct Importer<'a> {
python_ast: &'a Suite,
locator: &'a Locator<'a>,
stylist: &'a Stylist<'a>,
/// A map from module name to top-level `StmtKind::ImportFrom` statements.
import_from_map: FxHashMap<&'a str, &'a Stmt>,
/// The last top-level import statement.
trailing_import: Option<&'a Stmt>,
}

impl<'a> Importer<'a> {
pub fn new(python_ast: &'a Suite, locator: &'a Locator<'a>, stylist: &'a Stylist<'a>) -> Self {
Self {
python_ast,
locator,
stylist,
import_from_map: FxHashMap::default(),
trailing_import: None,
}
}

/// Visit a top-level import statement.
pub fn visit_import(&mut self, import: &'a Stmt) {
// Store a reference to the import statement in the appropriate map.
match &import.node {
StmtKind::Import { .. } => {
// Nothing to do here, we don't extend top-level `import` statements at all.
}
StmtKind::ImportFrom { module, level, .. } => {
// Store a reverse-map from module name to `import ... from` statement.
if level.map_or(true, |level| level == 0) {
if let Some(module) = module {
self.import_from_map.insert(module.as_str(), import);
}
}
}
_ => {
unreachable!("Expected StmtKind::Import | StmtKind::ImportFrom");
}
}

// Store a reference to the last top-level import statement.
self.trailing_import = Some(import);
}

/// Add an import statement to import the given module.
///
/// If there are no existing imports, the new import will be added at the top
/// of the file. Otherwise, it will be added after the most recent top-level
/// import statement.
pub fn add_import(&self, import: &AnyImport) -> Edit {
let required_import = import.to_string();
if let Some(stmt) = self.trailing_import {
// Insert after the last top-level import.
let (prefix, location, suffix) =
end_of_statement_insertion(stmt, self.locator, self.stylist);
let content = format!("{prefix}{required_import}{suffix}");
Edit::insertion(content, location)
} else {
// Insert at the top of the file.
let (prefix, location, suffix) =
top_of_file_insertion(self.python_ast, self.locator, self.stylist);
let content = format!("{prefix}{required_import}{suffix}");
Edit::insertion(content, location)
}
}

/// Return the top-level [`Stmt`] that imports the given module using `StmtKind::ImportFrom`.
/// if it exists.
pub fn get_import_from(&self, module: &str) -> Option<&Stmt> {
self.import_from_map.get(module).copied()
}

/// Add the given member to an existing `StmtKind::ImportFrom` statement.
pub fn add_member(&self, stmt: &Stmt, member: &str) -> Result<Edit> {
let mut tree = match_module(self.locator.slice(stmt))?;
let import_from = match_import_from(&mut tree)?;
let aliases = match_aliases(import_from)?;
aliases.push(ImportAlias {
name: NameOrAttribute::N(Box::new(Name {
value: member,
lpar: vec![],
rpar: vec![],
})),
asname: None,
comma: aliases.last().and_then(|alias| alias.comma.clone()),
});
let mut state = CodegenState {
default_newline: self.stylist.line_ending(),
default_indent: self.stylist.indentation(),
..CodegenState::default()
};
tree.codegen(&mut state);
Ok(Edit::replacement(
state.to_string(),
stmt.location,
stmt.end_location.unwrap(),
))
}
}

/// Find the end of the last docstring.
fn match_docstring_end(body: &[Stmt]) -> Option<Location> {
let mut iter = body.iter();
let Some(mut stmt) = iter.next() else {
return None;
};
if !is_docstring_stmt(stmt) {
return None;
}
for next in iter {
if !is_docstring_stmt(next) {
break;
}
stmt = next;
}
Some(stmt.end_location.unwrap())
}

/// Find the location at which a "top-of-file" import should be inserted,
/// along with a prefix and suffix to use for the insertion.
///
/// For example, given the following code:
///
/// ```python
/// """Hello, world!"""
///
/// import os
/// ```
///
/// The location returned will be the start of the `import os` statement,
/// along with a trailing newline suffix.
pub fn end_of_statement_insertion(
stmt: &Stmt,
locator: &Locator,
stylist: &Stylist,
) -> (&'static str, Location, &'static str) {
let location = stmt.end_location.unwrap();
let mut tokens = lexer::lex_located(locator.skip(location), Mode::Module, location).flatten();
if let Some((.., Tok::Semi, end)) = tokens.next() {
// If the first token after the docstring is a semicolon, insert after the semicolon as an
// inline statement;
(" ", end, ";")
} else {
// Otherwise, insert on the next line.
(
"",
Location::new(location.row() + 1, 0),
stylist.line_ending().as_str(),
)
}
}

/// Find the location at which a "top-of-file" import should be inserted,
/// along with a prefix and suffix to use for the insertion.
///
/// For example, given the following code:
///
/// ```python
/// """Hello, world!"""
///
/// import os
/// ```
///
/// The location returned will be the start of the `import os` statement,
/// along with a trailing newline suffix.
pub fn top_of_file_insertion(
body: &[Stmt],
locator: &Locator,
stylist: &Stylist,
) -> (&'static str, Location, &'static str) {
// Skip over any docstrings.
let mut location = if let Some(location) = match_docstring_end(body) {
let mut tokens = lexer::lex_located(locator.skip(location), Mode::Module, location)
.flatten()
.peekable();

// If the first token after the docstring is a semicolon, insert after the semicolon as an
// inline statement;
if let Some((.., Tok::Semi, end)) = tokens.peek() {
return (" ", *end, ";");
}

// Otherwise, advance to the next row.
Location::new(location.row() + 1, 0)
} else {
Location::default()
};

// Skip over any comments and empty lines.
for (.., tok, end) in
lexer::lex_located(locator.skip(location), Mode::Module, location).flatten()
{
if matches!(tok, Tok::Comment(..) | Tok::Newline) {
location = Location::new(end.row() + 1, 0);
} else {
break;
}
}

return ("", location, stylist.line_ending().as_str());
}

0 comments on commit 808cb2d

Please sign in to comment.