Skip to content

Commit d9cab4d

Browse files
Add support for specifying minimum dots in detected string imports (#19538)
## Summary Defaults to requiring two dots, which matches the Pants default.
1 parent d77b731 commit d9cab4d

File tree

13 files changed

+129
-42
lines changed

13 files changed

+129
-42
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/ruff/src/args.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,9 @@ pub struct AnalyzeGraphCommand {
169169
/// Attempt to detect imports from string literals.
170170
#[clap(long)]
171171
detect_string_imports: bool,
172+
/// The minimum number of dots in a string import to consider it a valid import.
173+
#[clap(long)]
174+
min_dots: Option<usize>,
172175
/// Enable preview mode. Use `--no-preview` to disable.
173176
#[arg(long, overrides_with("no_preview"))]
174177
preview: bool,
@@ -808,6 +811,7 @@ impl AnalyzeGraphCommand {
808811
} else {
809812
None
810813
},
814+
string_imports_min_dots: self.min_dots,
811815
preview: resolve_bool_arg(self.preview, self.no_preview).map(PreviewMode::from),
812816
target_version: self.target_version.map(ast::PythonVersion::from),
813817
..ExplicitConfigOverrides::default()
@@ -1305,6 +1309,7 @@ struct ExplicitConfigOverrides {
13051309
show_fixes: Option<bool>,
13061310
extension: Option<Vec<ExtensionPair>>,
13071311
detect_string_imports: Option<bool>,
1312+
string_imports_min_dots: Option<usize>,
13081313
}
13091314

13101315
impl ConfigurationTransformer for ExplicitConfigOverrides {
@@ -1392,6 +1397,9 @@ impl ConfigurationTransformer for ExplicitConfigOverrides {
13921397
if let Some(detect_string_imports) = &self.detect_string_imports {
13931398
config.analyze.detect_string_imports = Some(*detect_string_imports);
13941399
}
1400+
if let Some(string_imports_min_dots) = &self.string_imports_min_dots {
1401+
config.analyze.string_imports_min_dots = Some(*string_imports_min_dots);
1402+
}
13951403

13961404
config
13971405
}

crates/ruff/src/commands/analyze_graph.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ pub(crate) fn analyze_graph(
102102

103103
// Resolve the per-file settings.
104104
let settings = resolver.resolve(path);
105-
let string_imports = settings.analyze.detect_string_imports;
105+
let string_imports = settings.analyze.string_imports;
106106
let include_dependencies = settings.analyze.include_dependencies.get(path).cloned();
107107

108108
// Skip excluded files.

crates/ruff/tests/analyze_graph.rs

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -197,23 +197,43 @@ fn string_detection() -> Result<()> {
197197
insta::with_settings!({
198198
filters => INSTA_FILTERS.to_vec(),
199199
}, {
200-
assert_cmd_snapshot!(command().arg("--detect-string-imports").current_dir(&root), @r###"
201-
success: true
202-
exit_code: 0
203-
----- stdout -----
204-
{
205-
"ruff/__init__.py": [],
206-
"ruff/a.py": [
207-
"ruff/b.py"
208-
],
209-
"ruff/b.py": [
210-
"ruff/c.py"
211-
],
212-
"ruff/c.py": []
213-
}
200+
assert_cmd_snapshot!(command().arg("--detect-string-imports").current_dir(&root), @r#"
201+
success: true
202+
exit_code: 0
203+
----- stdout -----
204+
{
205+
"ruff/__init__.py": [],
206+
"ruff/a.py": [
207+
"ruff/b.py"
208+
],
209+
"ruff/b.py": [],
210+
"ruff/c.py": []
211+
}
214212
215-
----- stderr -----
216-
"###);
213+
----- stderr -----
214+
"#);
215+
});
216+
217+
insta::with_settings!({
218+
filters => INSTA_FILTERS.to_vec(),
219+
}, {
220+
assert_cmd_snapshot!(command().arg("--detect-string-imports").arg("--min-dots").arg("1").current_dir(&root), @r#"
221+
success: true
222+
exit_code: 0
223+
----- stdout -----
224+
{
225+
"ruff/__init__.py": [],
226+
"ruff/a.py": [
227+
"ruff/b.py"
228+
],
229+
"ruff/b.py": [
230+
"ruff/c.py"
231+
],
232+
"ruff/c.py": []
233+
}
234+
235+
----- stderr -----
236+
"#);
217237
});
218238

219239
Ok(())

crates/ruff/tests/lint.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2422,7 +2422,7 @@ requires-python = ">= 3.11"
24222422
analyze.exclude = []
24232423
analyze.preview = disabled
24242424
analyze.target_version = 3.11
2425-
analyze.detect_string_imports = false
2425+
analyze.string_imports = disabled
24262426
analyze.extension = ExtensionMapping({})
24272427
analyze.include_dependencies = {}
24282428
@@ -2734,7 +2734,7 @@ requires-python = ">= 3.11"
27342734
analyze.exclude = []
27352735
analyze.preview = disabled
27362736
analyze.target_version = 3.10
2737-
analyze.detect_string_imports = false
2737+
analyze.string_imports = disabled
27382738
analyze.extension = ExtensionMapping({})
27392739
analyze.include_dependencies = {}
27402740
@@ -3098,7 +3098,7 @@ from typing import Union;foo: Union[int, str] = 1
30983098
analyze.exclude = []
30993099
analyze.preview = disabled
31003100
analyze.target_version = 3.11
3101-
analyze.detect_string_imports = false
3101+
analyze.string_imports = disabled
31023102
analyze.extension = ExtensionMapping({})
31033103
analyze.include_dependencies = {}
31043104
@@ -3478,7 +3478,7 @@ from typing import Union;foo: Union[int, str] = 1
34783478
analyze.exclude = []
34793479
analyze.preview = disabled
34803480
analyze.target_version = 3.11
3481-
analyze.detect_string_imports = false
3481+
analyze.string_imports = disabled
34823482
analyze.extension = ExtensionMapping({})
34833483
analyze.include_dependencies = {}
34843484
@@ -3806,7 +3806,7 @@ from typing import Union;foo: Union[int, str] = 1
38063806
analyze.exclude = []
38073807
analyze.preview = disabled
38083808
analyze.target_version = 3.10
3809-
analyze.detect_string_imports = false
3809+
analyze.string_imports = disabled
38103810
analyze.extension = ExtensionMapping({})
38113811
analyze.include_dependencies = {}
38123812
@@ -4134,7 +4134,7 @@ from typing import Union;foo: Union[int, str] = 1
41344134
analyze.exclude = []
41354135
analyze.preview = disabled
41364136
analyze.target_version = 3.9
4137-
analyze.detect_string_imports = false
4137+
analyze.string_imports = disabled
41384138
analyze.extension = ExtensionMapping({})
41394139
analyze.include_dependencies = {}
41404140
@@ -4419,7 +4419,7 @@ from typing import Union;foo: Union[int, str] = 1
44194419
analyze.exclude = []
44204420
analyze.preview = disabled
44214421
analyze.target_version = 3.9
4422-
analyze.detect_string_imports = false
4422+
analyze.string_imports = disabled
44234423
analyze.extension = ExtensionMapping({})
44244424
analyze.include_dependencies = {}
44254425
@@ -4757,7 +4757,7 @@ from typing import Union;foo: Union[int, str] = 1
47574757
analyze.exclude = []
47584758
analyze.preview = disabled
47594759
analyze.target_version = 3.10
4760-
analyze.detect_string_imports = false
4760+
analyze.string_imports = disabled
47614761
analyze.extension = ExtensionMapping({})
47624762
analyze.include_dependencies = {}
47634763

crates/ruff/tests/snapshots/show_settings__display_default_settings.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -392,7 +392,7 @@ formatter.docstring_code_line_width = dynamic
392392
analyze.exclude = []
393393
analyze.preview = disabled
394394
analyze.target_version = 3.7
395-
analyze.detect_string_imports = false
395+
analyze.string_imports = disabled
396396
analyze.extension = ExtensionMapping({})
397397
analyze.include_dependencies = {}
398398

crates/ruff_graph/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ ty_python_semantic = { workspace = true }
2020

2121
anyhow = { workspace = true }
2222
clap = { workspace = true, optional = true }
23+
memchr = { workspace = true }
2324
salsa = { workspace = true }
2425
schemars = { workspace = true, optional = true }
2526
serde = { workspace = true, optional = true }

crates/ruff_graph/src/collector.rs

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::StringImports;
12
use ruff_python_ast::visitor::source_order::{
23
SourceOrderVisitor, walk_expr, walk_module, walk_stmt,
34
};
@@ -10,13 +11,13 @@ pub(crate) struct Collector<'a> {
1011
/// The path to the current module.
1112
module_path: Option<&'a [String]>,
1213
/// Whether to detect imports from string literals.
13-
string_imports: bool,
14+
string_imports: StringImports,
1415
/// The collected imports from the Python AST.
1516
imports: Vec<CollectedImport>,
1617
}
1718

1819
impl<'a> Collector<'a> {
19-
pub(crate) fn new(module_path: Option<&'a [String]>, string_imports: bool) -> Self {
20+
pub(crate) fn new(module_path: Option<&'a [String]>, string_imports: StringImports) -> Self {
2021
Self {
2122
module_path,
2223
string_imports,
@@ -118,28 +119,34 @@ impl<'ast> SourceOrderVisitor<'ast> for Collector<'_> {
118119
| Stmt::Continue(_)
119120
| Stmt::IpyEscapeCommand(_) => {
120121
// Only traverse simple statements when string imports is enabled.
121-
if self.string_imports {
122+
if self.string_imports.enabled {
122123
walk_stmt(self, stmt);
123124
}
124125
}
125126
}
126127
}
127128

128129
fn visit_expr(&mut self, expr: &'ast Expr) {
129-
if self.string_imports {
130+
if self.string_imports.enabled {
130131
if let Expr::StringLiteral(ast::ExprStringLiteral {
131132
value,
132133
range: _,
133134
node_index: _,
134135
}) = expr
135136
{
136-
// Determine whether the string literal "looks like" an import statement: contains
137-
// a dot, and consists solely of valid Python identifiers.
138137
let value = value.to_str();
139-
if let Some(module_name) = ModuleName::new(value) {
140-
self.imports.push(CollectedImport::Import(module_name));
138+
// Determine whether the string literal "looks like" an import statement: contains
139+
// the requisite number of dots, and consists solely of valid Python identifiers.
140+
if self.string_imports.min_dots == 0
141+
|| memchr::memchr_iter(b'.', value.as_bytes()).count()
142+
>= self.string_imports.min_dots
143+
{
144+
if let Some(module_name) = ModuleName::new(value) {
145+
self.imports.push(CollectedImport::Import(module_name));
146+
}
141147
}
142148
}
149+
143150
walk_expr(self, expr);
144151
}
145152
}

crates/ruff_graph/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use ruff_python_parser::{Mode, ParseOptions, parse};
99
use crate::collector::Collector;
1010
pub use crate::db::ModuleDb;
1111
use crate::resolver::Resolver;
12-
pub use crate::settings::{AnalyzeSettings, Direction};
12+
pub use crate::settings::{AnalyzeSettings, Direction, StringImports};
1313

1414
mod collector;
1515
mod db;
@@ -26,7 +26,7 @@ impl ModuleImports {
2626
db: &ModuleDb,
2727
path: &SystemPath,
2828
package: Option<&SystemPath>,
29-
string_imports: bool,
29+
string_imports: StringImports,
3030
) -> Result<Self> {
3131
// Read and parse the source code.
3232
let source = std::fs::read_to_string(path)?;

crates/ruff_graph/src/settings.rs

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ pub struct AnalyzeSettings {
1111
pub exclude: FilePatternSet,
1212
pub preview: PreviewMode,
1313
pub target_version: PythonVersion,
14-
pub detect_string_imports: bool,
14+
pub string_imports: StringImports,
1515
pub include_dependencies: BTreeMap<PathBuf, (PathBuf, Vec<String>)>,
1616
pub extension: ExtensionMapping,
1717
}
@@ -26,7 +26,7 @@ impl fmt::Display for AnalyzeSettings {
2626
self.exclude,
2727
self.preview,
2828
self.target_version,
29-
self.detect_string_imports,
29+
self.string_imports,
3030
self.extension | debug,
3131
self.include_dependencies | debug,
3232
]
@@ -35,6 +35,31 @@ impl fmt::Display for AnalyzeSettings {
3535
}
3636
}
3737

38+
#[derive(Debug, Copy, Clone, CacheKey)]
39+
pub struct StringImports {
40+
pub enabled: bool,
41+
pub min_dots: usize,
42+
}
43+
44+
impl Default for StringImports {
45+
fn default() -> Self {
46+
Self {
47+
enabled: false,
48+
min_dots: 2,
49+
}
50+
}
51+
}
52+
53+
impl fmt::Display for StringImports {
54+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55+
if self.enabled {
56+
write!(f, "enabled (min_dots: {})", self.min_dots)
57+
} else {
58+
write!(f, "disabled")
59+
}
60+
}
61+
}
62+
3863
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, CacheKey)]
3964
#[cfg_attr(
4065
feature = "serde",

0 commit comments

Comments
 (0)