diff --git a/src/python/pants/backend/python/dependency_inference/parse_python_dependencies_test.py b/src/python/pants/backend/python/dependency_inference/parse_python_dependencies_test.py index 1a70536885d..9a4ab527883 100644 --- a/src/python/pants/backend/python/dependency_inference/parse_python_dependencies_test.py +++ b/src/python/pants/backend/python/dependency_inference/parse_python_dependencies_test.py @@ -329,6 +329,14 @@ def test_imports_from_strings(rule_runner: RuleRunner, min_dots: int) -> None: 'a.b.c.d.2Bar', 'a.2b.c.D', + # Explicitly ignored strings + 'w.x', # pants: no-infer-dep + 'w.x.Foo', # pants: no-infer-dep + 'w.x.y.z', # pants: no-infer-dep + 'w.x.y.z.Foo', # pants: no-infer-dep + 'w.x.y.z.FooBar', # pants: no-infer-dep + 'u.v.w.x.y.z.Baz', # pants: no-infer-dep + # Definitely invalid strings 'I/have/a/slash', 'I\\\\have\\\\backslashes', diff --git a/src/rust/engine/dep_inference/src/python/mod.rs b/src/rust/engine/dep_inference/src/python/mod.rs index 64045004aaf..8c82d2ca234 100644 --- a/src/rust/engine/dep_inference/src/python/mod.rs +++ b/src/rust/engine/dep_inference/src/python/mod.rs @@ -107,18 +107,47 @@ impl ImportCollector<'_> { .trim_matches(|c| "'\"".contains(c)) } + fn is_pragma_ignored_at_row(&self, node: tree_sitter::Node, end_row: usize) -> bool { + let node_range = node.range(); + if node.kind_id() == KindID::COMMENT + && end_row == node_range.start_point.row + && self.code_at(node_range).contains("# pants: no-infer-dep") + { + return true; + } + false + } + fn is_pragma_ignored(&self, node: tree_sitter::Node) -> bool { if let Some(sibling) = node.next_named_sibling() { - let next_node_range = sibling.range(); - if sibling.kind_id() == KindID::COMMENT - && node.range().end_point.row == next_node_range.start_point.row - && self - .code_at(next_node_range) - .contains("# pants: no-infer-dep") - { + return self.is_pragma_ignored_at_row(sibling, node.range().end_point.row); + } + false + } + + fn is_pragma_ignored_recursive(&self, node: tree_sitter::Node) -> bool { + let node_end_point = node.range().end_point; + if let Some(sibling) = node.next_named_sibling() { + if self.is_pragma_ignored_at_row(sibling, node_end_point.row) { return true; } } + + let mut current = node; + loop { + if let Some(parent) = current.parent() { + if let Some(sibling) = parent.next_named_sibling() { + if self.is_pragma_ignored_at_row(sibling, node_end_point.row) { + return true; + } + } + current = parent; + continue; + } + // At the root / no more parents. + break; + } + false } @@ -322,7 +351,9 @@ impl Visitor for ImportCollector<'_> { fn visit_string(&mut self, node: tree_sitter::Node) -> ChildBehavior { let range = node.range(); let text: &str = self.string_at(range); - if !text.contains(|c: char| c.is_ascii_whitespace() || c == '\\') { + if !text.contains(|c: char| c.is_ascii_whitespace() || c == '\\') + && !self.is_pragma_ignored_recursive(node) + { self.string_candidates .insert(text.to_string(), (range.start_point.row + 1) as u64); } diff --git a/src/rust/engine/dep_inference/src/python/tests.rs b/src/rust/engine/dep_inference/src/python/tests.rs index 3036bdc0bd7..e550a2253fe 100644 --- a/src/rust/engine/dep_inference/src/python/tests.rs +++ b/src/rust/engine/dep_inference/src/python/tests.rs @@ -685,6 +685,32 @@ fn string_candidates() { // Technically the value of the string doesn't contain whitespace, but the parser isn't that // sophisticated yet. assert_strings("'''\\\na'''", &[]); + + // pragma ignored strings + assert_strings("'a' # pants: no-infer-dep", &[]); + assert_strings("'''a''' # pants: no-infer-dep", &[]); + assert_strings("'a.b' # pants: no-infer-dep", &[]); + assert_strings("'a.b.c_狗' # pants: no-infer-dep", &[]); + assert_strings("'..a.b.c.d' # pants: no-infer-dep", &[]); + assert_strings("['a.b'] # pants: no-infer-dep", &[]); + assert_strings("[{'a.b': 1}] # pants: no-infer-dep", &[]); + assert_strings("[{('a.b',): 1}] # pants: no-infer-dep", &[]); + assert_strings("[{2: 'a.b'}] # pants: no-infer-dep", &[]); + assert_strings("[{2: ('a.b',)}] # pants: no-infer-dep", &[]); + assert_strings("print('a.b') # pants: no-infer-dep", &[]); + assert_strings("print('a.b' if x else 3) # pants: no-infer-dep", &[]); + assert_strings("print(3 if x else 'a.b') # pants: no-infer-dep", &[]); + assert_strings( + "print([a for a in b if a not in ['a.b', 'c.d']]) # pants: no-infer-dep", + &[], + ); + assert_strings( + r" + for a, b in foo['a.b'].items(): # pants: no-infer-dep + pass + ", + &[], + ); } #[test]