mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-03 18:28:56 +00:00
Support relative paths for typing-modules (#2615)
This commit is contained in:
parent
3bca987665
commit
097c679cf3
9 changed files with 184 additions and 26 deletions
0
crates/ruff/resources/test/fixtures/pyflakes/project/foo/__init__.py
vendored
Normal file
0
crates/ruff/resources/test/fixtures/pyflakes/project/foo/__init__.py
vendored
Normal file
19
crates/ruff/resources/test/fixtures/pyflakes/project/foo/bar.py
vendored
Normal file
19
crates/ruff/resources/test/fixtures/pyflakes/project/foo/bar.py
vendored
Normal 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"]
|
0
crates/ruff/resources/test/fixtures/pyflakes/project/foo/bop/__init__.py
vendored
Normal file
0
crates/ruff/resources/test/fixtures/pyflakes/project/foo/bop/__init__.py
vendored
Normal file
26
crates/ruff/resources/test/fixtures/pyflakes/project/foo/bop/baz.py
vendored
Normal file
26
crates/ruff/resources/test/fixtures/pyflakes/project/foo/bop/baz.py
vendored
Normal 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"]
|
|
@ -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> {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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]) {
|
||||
|
|
|
@ -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: ~
|
||||
|
|
@ -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: ~
|
||||
|
Loading…
Add table
Add a link
Reference in a new issue