Resolve classes and functions relative to script name (#10965)

## Summary

If the user is analyzing a script (i.e., we have no module path), it
seems reasonable to use the script name when trying to identify paths to
objects defined _within_ the script.

Closes https://github.com/astral-sh/ruff/issues/10960.

## Test Plan

Ran:

```shell
check --isolated --select=B008 \
    --config 'lint.flake8-bugbear.extend-immutable-calls=["test.A"]' \
    test.py
```

On:

```python
class A: pass

def f(a=A()):
    pass
```
This commit is contained in:
Charlie Marsh 2024-04-17 21:42:50 -04:00 committed by GitHub
parent 1480d72643
commit b23414e3cc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 127 additions and 74 deletions

View file

@ -1,11 +1,10 @@
use std::path::Path;
use ruff_python_ast::{self as ast, Decorator};
use ruff_python_ast::helpers::map_callable;
use ruff_python_ast::name::{QualifiedName, UnqualifiedName};
use crate::model::SemanticModel;
use crate::{Module, ModuleSource};
#[derive(Debug, Clone, Copy, is_macro::Is)]
pub enum Visibility {
@ -134,44 +133,31 @@ fn stem(path: &str) -> &str {
}
}
/// A Python module can either be defined as a module path (i.e., the dot-separated path to the
/// module) or, if the module can't be resolved, as a file path (i.e., the path to the file defining
/// the module).
#[derive(Debug)]
pub enum ModuleSource<'a> {
/// A module path is a dot-separated path to the module.
Path(&'a [String]),
/// A file path is the path to the file defining the module, often a script outside of a
/// package.
File(&'a Path),
}
impl ModuleSource<'_> {
/// Return the `Visibility` of the module.
pub(crate) fn to_visibility(&self) -> Visibility {
match self {
Self::Path(path) => {
if path.iter().any(|m| is_private_module(m)) {
/// Infer the [`Visibility`] of a module from its path.
pub(crate) fn module_visibility(module: &Module) -> Visibility {
match &module.source {
ModuleSource::Path(path) => {
if path.iter().any(|m| is_private_module(m)) {
return Visibility::Private;
}
}
ModuleSource::File(path) => {
// Check to see if the filename itself indicates private visibility.
// Ex) `_foo.py` (but not `__init__.py`)
let mut components = path.iter().rev();
if let Some(filename) = components.next() {
let module_name = filename.to_string_lossy();
let module_name = stem(&module_name);
if is_private_module(module_name) {
return Visibility::Private;
}
}
Self::File(path) => {
// Check to see if the filename itself indicates private visibility.
// Ex) `_foo.py` (but not `__init__.py`)
let mut components = path.iter().rev();
if let Some(filename) = components.next() {
let module_name = filename.to_string_lossy();
let module_name = stem(&module_name);
if is_private_module(module_name) {
return Visibility::Private;
}
}
}
}
Visibility::Public
}
Visibility::Public
}
/// Infer the [`Visibility`] of a function from its name.
pub(crate) fn function_visibility(function: &ast::StmtFunctionDef) -> Visibility {
if function.name.starts_with('_') {
Visibility::Private
@ -180,6 +166,7 @@ pub(crate) fn function_visibility(function: &ast::StmtFunctionDef) -> Visibility
}
}
/// Infer the [`Visibility`] of a method from its name and decorators.
pub fn method_visibility(function: &ast::StmtFunctionDef) -> Visibility {
// Is this a setter or deleter?
if function.decorator_list.iter().any(|decorator| {
@ -204,6 +191,7 @@ pub fn method_visibility(function: &ast::StmtFunctionDef) -> Visibility {
Visibility::Private
}
/// Infer the [`Visibility`] of a class from its name.
pub(crate) fn class_visibility(class: &ast::StmtClassDef) -> Visibility {
if class.name.starts_with('_') {
Visibility::Private

View file

@ -3,13 +3,14 @@
use std::fmt::Debug;
use std::ops::Deref;
use std::path::Path;
use ruff_index::{newtype_index, IndexSlice, IndexVec};
use ruff_python_ast::{self as ast, all::DunderAllName, Stmt};
use ruff_text_size::{Ranged, TextRange};
use crate::analyze::visibility::{
class_visibility, function_visibility, method_visibility, ModuleSource, Visibility,
class_visibility, function_visibility, method_visibility, module_visibility, Visibility,
};
/// Id uniquely identifying a definition in a program.
@ -24,7 +25,19 @@ impl DefinitionId {
}
}
#[derive(Debug, is_macro::Is)]
/// A Python module can either be defined as a module path (i.e., the dot-separated path to the
/// module) or, if the module can't be resolved, as a file path (i.e., the path to the file defining
/// the module).
#[derive(Debug, Copy, Clone)]
pub enum ModuleSource<'a> {
/// A module path is a dot-separated path to the module.
Path(&'a [String]),
/// A file path is the path to the file defining the module, often a script outside of a
/// package.
File(&'a Path),
}
#[derive(Debug, Copy, Clone, is_macro::Is)]
pub enum ModuleKind {
/// A Python file that represents a module within a package.
Module,
@ -33,15 +46,17 @@ pub enum ModuleKind {
}
/// A Python module.
#[derive(Debug)]
#[derive(Debug, Copy, Clone)]
pub struct Module<'a> {
pub kind: ModuleKind,
pub source: ModuleSource<'a>,
pub python_ast: &'a [Stmt],
pub name: Option<&'a str>,
}
impl<'a> Module<'a> {
pub fn path(&self) -> Option<&'a [String]> {
/// Return the fully-qualified path of the module.
pub const fn qualified_name(&self) -> Option<&'a [String]> {
if let ModuleSource::Path(path) = self.source {
Some(path)
} else {
@ -50,11 +65,8 @@ impl<'a> Module<'a> {
}
/// Return the name of the module.
pub fn name(&self) -> Option<&'a str> {
match self.source {
ModuleSource::Path(path) => path.last().map(Deref::deref),
ModuleSource::File(file) => file.file_stem().and_then(std::ffi::OsStr::to_str),
}
pub const fn name(&self) -> Option<&'a str> {
self.name
}
}
@ -196,7 +208,7 @@ impl<'a> Definitions<'a> {
// visibility.
let visibility = {
match &definition {
Definition::Module(module) => module.source.to_visibility(),
Definition::Module(module) => module_visibility(module),
Definition::Member(member) => match member.kind {
MemberKind::Class(class) => {
let parent = &definitions[member.parent];

View file

@ -28,7 +28,7 @@ use crate::Imported;
/// A semantic model for a Python module, to enable querying the module's semantic information.
pub struct SemanticModel<'a> {
typing_modules: &'a [String],
module_path: Option<&'a [String]>,
module: Module<'a>,
/// Stack of all AST nodes in the program.
nodes: Nodes<'a>,
@ -134,7 +134,7 @@ impl<'a> SemanticModel<'a> {
pub fn new(typing_modules: &'a [String], path: &Path, module: Module<'a>) -> Self {
Self {
typing_modules,
module_path: module.path(),
module,
nodes: Nodes::default(),
node_id: None,
branches: Branches::default(),
@ -791,7 +791,11 @@ impl<'a> SemanticModel<'a> {
.first()
.map_or(false, |segment| *segment == ".")
{
from_relative_import(self.module_path?, qualified_name.segments(), tail)?
from_relative_import(
self.module.qualified_name()?,
qualified_name.segments(),
tail,
)?
} else {
qualified_name
.segments()
@ -817,14 +821,32 @@ impl<'a> SemanticModel<'a> {
}
}
BindingKind::ClassDefinition(_) | BindingKind::FunctionDefinition(_) => {
let value_name = UnqualifiedName::from_expr(value)?;
let resolved: QualifiedName = self
.module_path?
.iter()
.map(String::as_str)
.chain(value_name.segments().iter().copied())
.collect();
Some(resolved)
// If we have a fully-qualified path for the module, use it.
if let Some(path) = self.module.qualified_name() {
Some(
path.iter()
.map(String::as_str)
.chain(
UnqualifiedName::from_expr(value)?
.segments()
.iter()
.copied(),
)
.collect(),
)
} else {
// Otherwise, if we're in (e.g.) a script, use the module name.
Some(
std::iter::once(self.module.name()?)
.chain(
UnqualifiedName::from_expr(value)?
.segments()
.iter()
.copied(),
)
.collect(),
)
}
}
_ => None,
}