Support relative paths for typing-modules (#2615)

This commit is contained in:
Charlie Marsh 2023-02-06 19:51:37 -05:00 committed by GitHub
parent 3bca987665
commit 097c679cf3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 184 additions and 26 deletions

View file

@ -0,0 +1,19 @@
def f():
from typing import Literal
# OK
x: Literal["foo"]
def f():
from .typical import Literal
# OK
x: Literal["foo"]
def f():
from .atypical import Literal
# F821
x: Literal["foo"]

View file

@ -0,0 +1,26 @@
def f():
from typing import Literal
# OK
x: Literal["foo"]
def f():
from ..typical import Literal
# OK
x: Literal["foo"]
def f():
from .typical import Literal
# F821
x: Literal["foo"]
def f():
from .atypical import Literal
# F821
x: Literal["foo"]

View file

@ -1,3 +1,5 @@
use std::path::Path;
use itertools::Itertools;
use log::error;
use once_cell::sync::Lazy;
@ -10,7 +12,7 @@ use rustpython_ast::{
use rustpython_parser::lexer;
use rustpython_parser::lexer::Tok;
use rustpython_parser::token::StringKind;
use smallvec::smallvec;
use smallvec::{smallvec, SmallVec};
use crate::ast::types::{Binding, BindingKind, CallPath, Range};
use crate::ast::visitor;
@ -657,6 +659,38 @@ pub fn to_call_path(target: &str) -> CallPath {
}
}
/// Create a module path from a (package, path) pair.
///
/// For example, if the package is `foo/bar` and the path is `foo/bar/baz.py`, the call path is
/// `["baz"]`.
pub fn to_module_path(package: &Path, path: &Path) -> Option<Vec<String>> {
path.strip_prefix(package.parent()?)
.ok()?
.iter()
.map(Path::new)
.map(std::path::Path::file_stem)
.map(|path| path.and_then(|path| path.to_os_string().into_string().ok()))
.collect::<Option<Vec<String>>>()
}
/// Create a call path from a relative import.
pub fn from_relative_import<'a>(module: &'a [String], name: &'a str) -> CallPath<'a> {
let mut call_path: CallPath = SmallVec::with_capacity(module.len() + 1);
// Start with the module path.
call_path.extend(module.iter().map(String::as_str));
// Remove segments based on the number of dots.
for _ in 0..name.chars().take_while(|c| *c == '.').count() {
call_path.pop();
}
// Add the remaining segments.
call_path.extend(name.trim_start_matches('.').split('.'));
call_path
}
/// A [`Visitor`] that collects all return statements in a function or method.
#[derive(Default)]
pub struct ReturnStatementVisitor<'a> {

View file

@ -19,7 +19,9 @@ use smallvec::smallvec;
use ruff_python::builtins::{BUILTINS, MAGIC_GLOBALS};
use ruff_python::typing::TYPING_EXTENSIONS;
use crate::ast::helpers::{binding_range, collect_call_path, extract_handler_names};
use crate::ast::helpers::{
binding_range, collect_call_path, extract_handler_names, from_relative_import, to_module_path,
};
use crate::ast::operations::{extract_all_names, AllNamesFlags};
use crate::ast::relocate::relocate_expr;
use crate::ast::types::{
@ -55,6 +57,7 @@ type DeferralContext<'a> = (Vec<usize>, Vec<RefEquality<'a, Stmt>>);
pub struct Checker<'a> {
// Input data.
pub(crate) path: &'a Path,
module_path: Option<Vec<String>>,
package: Option<&'a Path>,
autofix: flags::Autofix,
noqa: flags::Noqa,
@ -119,6 +122,7 @@ impl<'a> Checker<'a> {
noqa: flags::Noqa,
path: &'a Path,
package: Option<&'a Path>,
module_path: Option<Vec<String>>,
locator: &'a Locator,
style: &'a Stylist,
indexer: &'a Indexer,
@ -130,6 +134,7 @@ impl<'a> Checker<'a> {
noqa,
path,
package,
module_path,
locator,
stylist: style,
indexer,
@ -234,32 +239,36 @@ impl<'a> Checker<'a> {
if let Some(head) = call_path.first() {
if let Some(binding) = self.find_binding(head) {
match &binding.kind {
BindingKind::Importation(.., name) => {
// Ignore relative imports.
if name.starts_with('.') {
return None;
}
let mut source_path: CallPath = name.split('.').collect();
source_path.extend(call_path.into_iter().skip(1));
return Some(source_path);
}
BindingKind::SubmoduleImportation(name, ..) => {
// Ignore relative imports.
if name.starts_with('.') {
return None;
}
let mut source_path: CallPath = name.split('.').collect();
source_path.extend(call_path.into_iter().skip(1));
return Some(source_path);
BindingKind::Importation(.., name)
| BindingKind::SubmoduleImportation(name, ..) => {
return if name.starts_with('.') {
if let Some(module) = &self.module_path {
let mut source_path = from_relative_import(module, name);
source_path.extend(call_path.into_iter().skip(1));
Some(source_path)
} else {
None
}
} else {
let mut source_path: CallPath = name.split('.').collect();
source_path.extend(call_path.into_iter().skip(1));
Some(source_path)
};
}
BindingKind::FromImportation(.., name) => {
// Ignore relative imports.
if name.starts_with('.') {
return None;
}
let mut source_path: CallPath = name.split('.').collect();
source_path.extend(call_path.into_iter().skip(1));
return Some(source_path);
return if name.starts_with('.') {
if let Some(module) = &self.module_path {
let mut source_path = from_relative_import(module, name);
source_path.extend(call_path.into_iter().skip(1));
Some(source_path)
} else {
None
}
} else {
let mut source_path: CallPath = name.split('.').collect();
source_path.extend(call_path.into_iter().skip(1));
Some(source_path)
};
}
BindingKind::Builtin => {
let mut source_path: CallPath = smallvec![];
@ -5285,6 +5294,7 @@ pub fn check_ast(
noqa,
path,
package,
package.and_then(|package| to_module_path(package, path)),
locator,
stylist,
indexer,

View file

@ -207,6 +207,32 @@ mod tests {
Ok(())
}
#[test]
fn relative_typing_module() -> Result<()> {
let diagnostics = test_path(
Path::new("pyflakes/project/foo/bar.py"),
&settings::Settings {
typing_modules: vec!["foo.typical".to_string()],
..settings::Settings::for_rules(vec![Rule::UndefinedName])
},
)?;
assert_yaml_snapshot!(diagnostics);
Ok(())
}
#[test]
fn nested_relative_typing_module() -> Result<()> {
let diagnostics = test_path(
Path::new("pyflakes/project/foo/bop/baz.py"),
&settings::Settings {
typing_modules: vec!["foo.typical".to_string()],
..settings::Settings::for_rules(vec![Rule::UndefinedName])
},
)?;
assert_yaml_snapshot!(diagnostics);
Ok(())
}
/// A re-implementation of the Pyflakes test runner.
/// Note that all tests marked with `#[ignore]` should be considered TODOs.
fn flakes(contents: &str, expected: &[Rule]) {

View file

@ -0,0 +1,27 @@
---
source: crates/ruff/src/rules/pyflakes/mod.rs
expression: diagnostics
---
- kind:
UndefinedName:
name: foo
location:
row: 19
column: 15
end_location:
row: 19
column: 20
fix: ~
parent: ~
- kind:
UndefinedName:
name: foo
location:
row: 26
column: 15
end_location:
row: 26
column: 20
fix: ~
parent: ~

View file

@ -0,0 +1,16 @@
---
source: crates/ruff/src/rules/pyflakes/mod.rs
expression: diagnostics
---
- kind:
UndefinedName:
name: foo
location:
row: 19
column: 15
end_location:
row: 19
column: 20
fix: ~
parent: ~