Skip to content

Commit b23414e

Browse files
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 #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 ```
1 parent 1480d72 commit b23414e

File tree

8 files changed

+126
-73
lines changed

8 files changed

+126
-73
lines changed

crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B008_extended.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,15 @@ def okay(data: custom.ImmutableTypeA = foo()):
2323

2424
def error_due_to_missing_import(data: List[str] = Depends(None)):
2525
...
26+
27+
28+
class Class:
29+
pass
30+
31+
32+
def okay(obj=Class()):
33+
...
34+
35+
36+
def error(obj=OtherClass()):
37+
...

crates/ruff_linter/src/checkers/ast/analyze/statement.rs

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -612,7 +612,8 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
612612
pylint::rules::manual_from_import(checker, stmt, alias, names);
613613
}
614614
if checker.enabled(Rule::ImportSelf) {
615-
if let Some(diagnostic) = pylint::rules::import_self(alias, checker.module_path)
615+
if let Some(diagnostic) =
616+
pylint::rules::import_self(alias, checker.module.qualified_name())
616617
{
617618
checker.diagnostics.push(diagnostic);
618619
}
@@ -776,9 +777,11 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
776777
flake8_bandit::rules::suspicious_imports(checker, stmt);
777778
}
778779
if checker.enabled(Rule::BannedApi) {
779-
if let Some(module) =
780-
helpers::resolve_imported_module_path(level, module, checker.module_path)
781-
{
780+
if let Some(module) = helpers::resolve_imported_module_path(
781+
level,
782+
module,
783+
checker.module.qualified_name(),
784+
) {
782785
flake8_tidy_imports::rules::banned_api(
783786
checker,
784787
&flake8_tidy_imports::matchers::NameMatchPolicy::MatchNameOrParent(
@@ -805,9 +808,11 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
805808
}
806809
}
807810
if checker.enabled(Rule::BannedModuleLevelImports) {
808-
if let Some(module) =
809-
helpers::resolve_imported_module_path(level, module, checker.module_path)
810-
{
811+
if let Some(module) = helpers::resolve_imported_module_path(
812+
level,
813+
module,
814+
checker.module.qualified_name(),
815+
) {
811816
flake8_tidy_imports::rules::banned_module_level_imports(
812817
checker,
813818
&flake8_tidy_imports::matchers::NameMatchPolicy::MatchNameOrParent(
@@ -894,7 +899,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
894899
stmt,
895900
level,
896901
module,
897-
checker.module_path,
902+
checker.module.qualified_name(),
898903
checker.settings.flake8_tidy_imports.ban_relative_imports,
899904
) {
900905
checker.diagnostics.push(diagnostic);
@@ -993,9 +998,12 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
993998
}
994999
}
9951000
if checker.enabled(Rule::ImportSelf) {
996-
if let Some(diagnostic) =
997-
pylint::rules::import_from_self(level, module, names, checker.module_path)
998-
{
1001+
if let Some(diagnostic) = pylint::rules::import_from_self(
1002+
level,
1003+
module,
1004+
names,
1005+
checker.module.qualified_name(),
1006+
) {
9991007
checker.diagnostics.push(diagnostic);
10001008
}
10011009
}

crates/ruff_linter/src/checkers/ast/mod.rs

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,11 @@ use ruff_python_ast::{helpers, str, visitor, PySourceType};
5050
use ruff_python_codegen::{Generator, Stylist};
5151
use ruff_python_index::Indexer;
5252
use ruff_python_parser::typing::{parse_type_annotation, AnnotationKind};
53-
use ruff_python_semantic::analyze::{imports, typing, visibility};
53+
use ruff_python_semantic::analyze::{imports, typing};
5454
use ruff_python_semantic::{
5555
BindingFlags, BindingId, BindingKind, Exceptions, Export, FromImport, Globals, Import, Module,
56-
ModuleKind, NodeId, ScopeId, ScopeKind, SemanticModel, SemanticModelFlags, StarImport,
57-
SubmoduleImport,
56+
ModuleKind, ModuleSource, NodeId, ScopeId, ScopeKind, SemanticModel, SemanticModelFlags,
57+
StarImport, SubmoduleImport,
5858
};
5959
use ruff_python_stdlib::builtins::{IPYTHON_BUILTINS, MAGIC_GLOBALS, PYTHON_BUILTINS};
6060
use ruff_source_file::{Locator, OneIndexed, SourceRow};
@@ -110,7 +110,7 @@ pub(crate) struct Checker<'a> {
110110
/// The [`Path`] to the package containing the current file.
111111
package: Option<&'a Path>,
112112
/// The module representation of the current file (e.g., `foo.bar`).
113-
module_path: Option<&'a [String]>,
113+
module: Module<'a>,
114114
/// The [`PySourceType`] of the current file.
115115
pub(crate) source_type: PySourceType,
116116
/// The [`CellOffsets`] for the current file, if it's a Jupyter notebook.
@@ -174,7 +174,7 @@ impl<'a> Checker<'a> {
174174
noqa,
175175
path,
176176
package,
177-
module_path: module.path(),
177+
module,
178178
source_type,
179179
locator,
180180
stylist,
@@ -2282,10 +2282,15 @@ pub(crate) fn check_ast(
22822282
} else {
22832283
ModuleKind::Module
22842284
},
2285+
name: if let Some(module_path) = &module_path {
2286+
module_path.last().map(String::as_str)
2287+
} else {
2288+
path.file_stem().and_then(std::ffi::OsStr::to_str)
2289+
},
22852290
source: if let Some(module_path) = module_path.as_ref() {
2286-
visibility::ModuleSource::Path(module_path)
2291+
ModuleSource::Path(module_path)
22872292
} else {
2288-
visibility::ModuleSource::File(path)
2293+
ModuleSource::File(path)
22892294
},
22902295
python_ast,
22912296
};

crates/ruff_linter/src/rules/flake8_bugbear/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ mod tests {
114114
"fastapi.Depends".to_string(),
115115
"fastapi.Query".to_string(),
116116
"custom.ImmutableTypeA".to_string(),
117+
"B008_extended.Class".to_string(),
117118
],
118119
},
119120
..LinterSettings::for_rule(Rule::FunctionCallInDefaultArgument)

crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__extend_immutable_calls_arg_default.snap

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,9 @@ B008_extended.py:24:51: B008 Do not perform function call `Depends` in argument
88
25 | ...
99
|
1010

11-
11+
B008_extended.py:36:15: B008 Do not perform function call `OtherClass` in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
12+
|
13+
36 | def error(obj=OtherClass()):
14+
| ^^^^^^^^^^^^ B008
15+
37 | ...
16+
|

crates/ruff_python_semantic/src/analyze/visibility.rs

Lines changed: 21 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
use std::path::Path;
2-
31
use ruff_python_ast::{self as ast, Decorator};
42

53
use ruff_python_ast::helpers::map_callable;
64
use ruff_python_ast::name::{QualifiedName, UnqualifiedName};
75

86
use crate::model::SemanticModel;
7+
use crate::{Module, ModuleSource};
98

109
#[derive(Debug, Clone, Copy, is_macro::Is)]
1110
pub enum Visibility {
@@ -134,44 +133,31 @@ fn stem(path: &str) -> &str {
134133
}
135134
}
136135

137-
/// A Python module can either be defined as a module path (i.e., the dot-separated path to the
138-
/// module) or, if the module can't be resolved, as a file path (i.e., the path to the file defining
139-
/// the module).
140-
#[derive(Debug)]
141-
pub enum ModuleSource<'a> {
142-
/// A module path is a dot-separated path to the module.
143-
Path(&'a [String]),
144-
/// A file path is the path to the file defining the module, often a script outside of a
145-
/// package.
146-
File(&'a Path),
147-
}
148-
149-
impl ModuleSource<'_> {
150-
/// Return the `Visibility` of the module.
151-
pub(crate) fn to_visibility(&self) -> Visibility {
152-
match self {
153-
Self::Path(path) => {
154-
if path.iter().any(|m| is_private_module(m)) {
155-
return Visibility::Private;
156-
}
136+
/// Infer the [`Visibility`] of a module from its path.
137+
pub(crate) fn module_visibility(module: &Module) -> Visibility {
138+
match &module.source {
139+
ModuleSource::Path(path) => {
140+
if path.iter().any(|m| is_private_module(m)) {
141+
return Visibility::Private;
157142
}
158-
Self::File(path) => {
159-
// Check to see if the filename itself indicates private visibility.
160-
// Ex) `_foo.py` (but not `__init__.py`)
161-
let mut components = path.iter().rev();
162-
if let Some(filename) = components.next() {
163-
let module_name = filename.to_string_lossy();
164-
let module_name = stem(&module_name);
165-
if is_private_module(module_name) {
166-
return Visibility::Private;
167-
}
143+
}
144+
ModuleSource::File(path) => {
145+
// Check to see if the filename itself indicates private visibility.
146+
// Ex) `_foo.py` (but not `__init__.py`)
147+
let mut components = path.iter().rev();
148+
if let Some(filename) = components.next() {
149+
let module_name = filename.to_string_lossy();
150+
let module_name = stem(&module_name);
151+
if is_private_module(module_name) {
152+
return Visibility::Private;
168153
}
169154
}
170155
}
171-
Visibility::Public
172156
}
157+
Visibility::Public
173158
}
174159

160+
/// Infer the [`Visibility`] of a function from its name.
175161
pub(crate) fn function_visibility(function: &ast::StmtFunctionDef) -> Visibility {
176162
if function.name.starts_with('_') {
177163
Visibility::Private
@@ -180,6 +166,7 @@ pub(crate) fn function_visibility(function: &ast::StmtFunctionDef) -> Visibility
180166
}
181167
}
182168

169+
/// Infer the [`Visibility`] of a method from its name and decorators.
183170
pub fn method_visibility(function: &ast::StmtFunctionDef) -> Visibility {
184171
// Is this a setter or deleter?
185172
if function.decorator_list.iter().any(|decorator| {
@@ -204,6 +191,7 @@ pub fn method_visibility(function: &ast::StmtFunctionDef) -> Visibility {
204191
Visibility::Private
205192
}
206193

194+
/// Infer the [`Visibility`] of a class from its name.
207195
pub(crate) fn class_visibility(class: &ast::StmtClassDef) -> Visibility {
208196
if class.name.starts_with('_') {
209197
Visibility::Private

crates/ruff_python_semantic/src/definition.rs

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@
33
44
use std::fmt::Debug;
55
use std::ops::Deref;
6+
use std::path::Path;
67

78
use ruff_index::{newtype_index, IndexSlice, IndexVec};
89
use ruff_python_ast::{self as ast, all::DunderAllName, Stmt};
910
use ruff_text_size::{Ranged, TextRange};
1011

1112
use crate::analyze::visibility::{
12-
class_visibility, function_visibility, method_visibility, ModuleSource, Visibility,
13+
class_visibility, function_visibility, method_visibility, module_visibility, Visibility,
1314
};
1415

1516
/// Id uniquely identifying a definition in a program.
@@ -24,7 +25,19 @@ impl DefinitionId {
2425
}
2526
}
2627

27-
#[derive(Debug, is_macro::Is)]
28+
/// A Python module can either be defined as a module path (i.e., the dot-separated path to the
29+
/// module) or, if the module can't be resolved, as a file path (i.e., the path to the file defining
30+
/// the module).
31+
#[derive(Debug, Copy, Clone)]
32+
pub enum ModuleSource<'a> {
33+
/// A module path is a dot-separated path to the module.
34+
Path(&'a [String]),
35+
/// A file path is the path to the file defining the module, often a script outside of a
36+
/// package.
37+
File(&'a Path),
38+
}
39+
40+
#[derive(Debug, Copy, Clone, is_macro::Is)]
2841
pub enum ModuleKind {
2942
/// A Python file that represents a module within a package.
3043
Module,
@@ -33,15 +46,17 @@ pub enum ModuleKind {
3346
}
3447

3548
/// A Python module.
36-
#[derive(Debug)]
49+
#[derive(Debug, Copy, Clone)]
3750
pub struct Module<'a> {
3851
pub kind: ModuleKind,
3952
pub source: ModuleSource<'a>,
4053
pub python_ast: &'a [Stmt],
54+
pub name: Option<&'a str>,
4155
}
4256

4357
impl<'a> Module<'a> {
44-
pub fn path(&self) -> Option<&'a [String]> {
58+
/// Return the fully-qualified path of the module.
59+
pub const fn qualified_name(&self) -> Option<&'a [String]> {
4560
if let ModuleSource::Path(path) = self.source {
4661
Some(path)
4762
} else {
@@ -50,11 +65,8 @@ impl<'a> Module<'a> {
5065
}
5166

5267
/// Return the name of the module.
53-
pub fn name(&self) -> Option<&'a str> {
54-
match self.source {
55-
ModuleSource::Path(path) => path.last().map(Deref::deref),
56-
ModuleSource::File(file) => file.file_stem().and_then(std::ffi::OsStr::to_str),
57-
}
68+
pub const fn name(&self) -> Option<&'a str> {
69+
self.name
5870
}
5971
}
6072

@@ -196,7 +208,7 @@ impl<'a> Definitions<'a> {
196208
// visibility.
197209
let visibility = {
198210
match &definition {
199-
Definition::Module(module) => module.source.to_visibility(),
211+
Definition::Module(module) => module_visibility(module),
200212
Definition::Member(member) => match member.kind {
201213
MemberKind::Class(class) => {
202214
let parent = &definitions[member.parent];

crates/ruff_python_semantic/src/model.rs

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ use crate::Imported;
2828
/// A semantic model for a Python module, to enable querying the module's semantic information.
2929
pub struct SemanticModel<'a> {
3030
typing_modules: &'a [String],
31-
module_path: Option<&'a [String]>,
31+
module: Module<'a>,
3232

3333
/// Stack of all AST nodes in the program.
3434
nodes: Nodes<'a>,
@@ -134,7 +134,7 @@ impl<'a> SemanticModel<'a> {
134134
pub fn new(typing_modules: &'a [String], path: &Path, module: Module<'a>) -> Self {
135135
Self {
136136
typing_modules,
137-
module_path: module.path(),
137+
module,
138138
nodes: Nodes::default(),
139139
node_id: None,
140140
branches: Branches::default(),
@@ -791,7 +791,11 @@ impl<'a> SemanticModel<'a> {
791791
.first()
792792
.map_or(false, |segment| *segment == ".")
793793
{
794-
from_relative_import(self.module_path?, qualified_name.segments(), tail)?
794+
from_relative_import(
795+
self.module.qualified_name()?,
796+
qualified_name.segments(),
797+
tail,
798+
)?
795799
} else {
796800
qualified_name
797801
.segments()
@@ -817,14 +821,32 @@ impl<'a> SemanticModel<'a> {
817821
}
818822
}
819823
BindingKind::ClassDefinition(_) | BindingKind::FunctionDefinition(_) => {
820-
let value_name = UnqualifiedName::from_expr(value)?;
821-
let resolved: QualifiedName = self
822-
.module_path?
823-
.iter()
824-
.map(String::as_str)
825-
.chain(value_name.segments().iter().copied())
826-
.collect();
827-
Some(resolved)
824+
// If we have a fully-qualified path for the module, use it.
825+
if let Some(path) = self.module.qualified_name() {
826+
Some(
827+
path.iter()
828+
.map(String::as_str)
829+
.chain(
830+
UnqualifiedName::from_expr(value)?
831+
.segments()
832+
.iter()
833+
.copied(),
834+
)
835+
.collect(),
836+
)
837+
} else {
838+
// Otherwise, if we're in (e.g.) a script, use the module name.
839+
Some(
840+
std::iter::once(self.module.name()?)
841+
.chain(
842+
UnqualifiedName::from_expr(value)?
843+
.segments()
844+
.iter()
845+
.copied(),
846+
)
847+
.collect(),
848+
)
849+
}
828850
}
829851
_ => None,
830852
}

0 commit comments

Comments
 (0)