1use brokk_bifrost_core::analyzer::Range;
14use brokk_bifrost_core::analyzer::semantic_diagnostics::contains_node as covers;
15use brokk_bifrost_core::analyzer::structural::spec::StructuralSpec;
16use brokk_bifrost_core::analyzer::work_budget::NodeBudget;
17use std::collections::{HashMap, HashSet};
18use tree_sitter::Node;
19
20use crate::bindings::{
21 PythonDirectScopeBindingKind, PythonLexicalNameResolution, PythonLexicalScopeInventory,
22 python_direct_scope_bindings_bounded, python_executable_scope_children,
23 python_name_resolution_at,
24};
25use crate::imports::python_import_bindings_from_tree;
26use crate::syntax::{python_parameter_name, python_plain_string_literal};
27
28const PROCESS_INPUT_CONTAINERS: [(&str, &str); 2] = [("os", "environ"), ("sys", "argv")];
36
37#[derive(Clone, Debug, Eq, PartialEq)]
42pub enum PythonRuntimeAccessKey {
43 Property(String),
45 Index(u128),
47 Dynamic,
49 Unsupported,
52}
53
54#[derive(Clone, Copy, Debug, Eq, PartialEq)]
56pub enum PythonRuntimeRootResolution {
57 ImportedModule,
60 LexicallyBound,
63 Unknown,
65}
66
67#[derive(Clone, Copy, Debug, Eq, PartialEq)]
69pub enum PythonRuntimeMutationEvidence {
70 NoKnownWrite,
71 KnownWrite,
72 KnownWriteAndUnknownEffects,
73 UnknownEffects,
74}
75
76#[derive(Clone, Copy, Debug, Eq, PartialEq)]
79pub enum PythonRuntimeAccessorCoverage {
80 NoKnownAccessorEffects,
81 UnknownAccessorEffects,
82}
83
84#[derive(Clone, Copy, Debug, Eq, PartialEq)]
86pub enum PythonRuntimeExecutionContext {
87 Module,
88 DeferredFunction(Range),
89}
90
91#[derive(Clone, Copy, Debug, Eq, PartialEq)]
93pub enum PythonRuntimeWriteTarget {
94 Container,
96 Key,
98 ContainerEscape,
101 ModuleEscape,
104}
105
106#[derive(Clone, Debug, Eq, PartialEq)]
108pub struct PythonRuntimeWrite {
109 pub root_name: String,
111 pub container: Option<String>,
114 pub range: Range,
115 pub target: PythonRuntimeWriteTarget,
116}
117
118#[derive(Clone, Debug, Eq, PartialEq)]
120pub struct PythonRuntimeRead {
121 pub binding_name: String,
123 pub root_name: String,
125 pub container: String,
126 pub access: PythonRuntimeAccessKey,
127 pub range: Range,
129 pub root_range: Range,
130 pub container_range: Range,
134 pub candidate_anchor: Range,
135 pub key_range: Range,
136 pub lexical_resolution: PythonRuntimeRootResolution,
137 pub mutation: PythonRuntimeMutationEvidence,
138 pub execution: PythonRuntimeExecutionContext,
139 pub accessor: PythonRuntimeAccessorCoverage,
143}
144
145#[derive(Clone, Debug, Eq, PartialEq)]
147pub struct PythonRuntimeReadFacts {
148 pub reads: Vec<PythonRuntimeRead>,
149 pub writes: Vec<PythonRuntimeWrite>,
150 pub visited_nodes: usize,
152 pub complete: bool,
155}
156
157struct ModuleBinding {
159 local_name: String,
160 root_module: String,
162 scope_start_byte: usize,
163 scope_end_byte: usize,
164 function_scoped: bool,
165}
166
167pub fn extract_python_runtime_reads(
173 root: Node<'_>,
174 source: &str,
175 max_facts: usize,
176) -> PythonRuntimeReadFacts {
177 let mut budget = NodeBudget::new(max_facts.saturating_mul(64).max(4096));
180 let bindings = module_bindings(root, source);
181 let mut reads = Vec::new();
182 let mut writes = Vec::new();
183 let mut stack = vec![root];
184 while let Some(node) = stack.pop() {
185 if !budget.visit() {
186 break;
187 }
188 if node.kind() == "attribute"
189 && reads.len() + writes.len() < max_facts
190 && let Some(path) = reviewed_attribute_path(node, source)
191 {
192 let resolution = root_resolution(
193 path.root,
194 &path.root_module,
195 &bindings,
196 root,
197 source,
198 &mut budget,
199 );
200 if resolution == PythonRuntimeRootResolution::ImportedModule {
201 classify_container_occurrence(
202 node,
203 &path,
204 source,
205 root,
206 &mut budget,
207 &mut reads,
208 &mut writes,
209 );
210 }
211 }
212 if node.kind() == "identifier"
213 && writes.len() + reads.len() < max_facts
214 && let Some(module) =
215 escaping_module_identifier(node, &bindings, root, source, &mut budget)
216 {
217 writes.push(PythonRuntimeWrite {
218 root_name: module,
219 container: None,
220 range: node_source_range(node),
221 target: PythonRuntimeWriteTarget::ModuleEscape,
222 });
223 }
224 let mut cursor = node.walk();
225 stack.extend(node.named_children(&mut cursor));
226 }
227 for read in &mut reads {
230 let known_write = writes.iter().any(|write| {
231 write.root_name == read.root_name
232 && write
233 .container
234 .as_ref()
235 .is_none_or(|container| *container == read.container)
236 });
237 read.mutation = match (known_write, read.accessor) {
238 (false, PythonRuntimeAccessorCoverage::NoKnownAccessorEffects) => {
239 PythonRuntimeMutationEvidence::NoKnownWrite
240 }
241 (false, PythonRuntimeAccessorCoverage::UnknownAccessorEffects) => {
242 PythonRuntimeMutationEvidence::UnknownEffects
243 }
244 (true, PythonRuntimeAccessorCoverage::NoKnownAccessorEffects) => {
245 PythonRuntimeMutationEvidence::KnownWrite
246 }
247 (true, PythonRuntimeAccessorCoverage::UnknownAccessorEffects) => {
248 PythonRuntimeMutationEvidence::KnownWriteAndUnknownEffects
249 }
250 };
251 }
252 reads.sort_by_key(|read| read.range.start_byte);
253 writes.sort_by_key(|write| write.range.start_byte);
254 PythonRuntimeReadFacts {
255 complete: !budget.exhausted() && reads.len() + writes.len() < max_facts,
256 visited_nodes: budget.visited(),
257 reads,
258 writes,
259 }
260}
261
262pub fn python_runtime_keyed_access_seed<'tree>(node: Node<'tree>) -> Option<Node<'tree>> {
270 if node.kind() != "subscript" {
271 return None;
272 }
273 let base = node.child_by_field_name("value")?;
274 if base.kind() != "attribute" {
275 return None;
276 }
277 let attribute = base.child_by_field_name("attribute")?;
278 let object = base.child_by_field_name("object")?;
279 if object.kind() != "identifier" || attribute.kind() != "identifier" {
280 return None;
281 }
282 Some(base)
283}
284
285struct ReviewedPath<'tree> {
287 root: Node<'tree>,
288 root_module: String,
289 container: String,
290}
291
292fn reviewed_attribute_path<'tree>(node: Node<'tree>, source: &str) -> Option<ReviewedPath<'tree>> {
295 let object = node.child_by_field_name("object")?;
296 let attribute = node.child_by_field_name("attribute")?;
297 if object.kind() != "identifier" || attribute.kind() != "identifier" {
298 return None;
299 }
300 let container = node_text(attribute, source);
301 let (root_module, _) = PROCESS_INPUT_CONTAINERS
302 .iter()
303 .find(|(_, reviewed)| *reviewed == container)?;
304 Some(ReviewedPath {
305 root: object,
306 root_module: (*root_module).to_owned(),
307 container: container.to_owned(),
308 })
309}
310
311fn classify_container_occurrence<'tree>(
314 attribute: Node<'tree>,
315 path: &ReviewedPath<'tree>,
316 source: &str,
317 module_root: Node<'tree>,
318 budget: &mut NodeBudget,
319 reads: &mut Vec<PythonRuntimeRead>,
320 writes: &mut Vec<PythonRuntimeWrite>,
321) {
322 let write = |target| PythonRuntimeWrite {
323 root_name: path.root_module.clone(),
324 container: Some(path.container.clone()),
325 range: node_source_range(attribute),
326 target,
327 };
328 let Some(parent) = attribute.parent() else {
329 writes.push(write(PythonRuntimeWriteTarget::ContainerEscape));
330 return;
331 };
332 if parent.kind() != "subscript"
333 || parent
334 .child_by_field_name("value")
335 .is_none_or(|value| value.id() != attribute.id())
336 {
337 writes.push(write(if is_binding_target(attribute) {
342 PythonRuntimeWriteTarget::Container
343 } else {
344 PythonRuntimeWriteTarget::ContainerEscape
345 }));
346 return;
347 }
348 if is_binding_target(parent) {
349 writes.push(write(PythonRuntimeWriteTarget::Key));
350 return;
351 }
352 let Some(subscript) = parent.child_by_field_name("subscript") else {
353 writes.push(write(PythonRuntimeWriteTarget::ContainerEscape));
354 return;
355 };
356 let mut cursor = parent.walk();
359 let access = if parent
360 .children(&mut cursor)
361 .any(|child| child.kind() == ",")
362 {
363 PythonRuntimeAccessKey::Unsupported
364 } else {
365 access_key(subscript, source)
366 };
367 let read_range = node_source_range(parent);
368 let execution = match enclosing_execution_context(attribute) {
369 Some(function) => {
370 PythonRuntimeExecutionContext::DeferredFunction(node_source_range(function))
371 }
372 None => PythonRuntimeExecutionContext::Module,
373 };
374 let accessor = execution_context_accessor_coverage(attribute, module_root, read_range, budget);
375 reads.push(PythonRuntimeRead {
376 binding_name: node_text(path.root, source).to_owned(),
377 root_name: path.root_module.clone(),
378 container: path.container.clone(),
379 access,
380 range: read_range,
381 root_range: node_source_range(path.root),
382 container_range: node_source_range(attribute),
383 candidate_anchor: node_source_range(attribute),
384 key_range: node_source_range(subscript),
385 lexical_resolution: PythonRuntimeRootResolution::ImportedModule,
386 mutation: PythonRuntimeMutationEvidence::UnknownEffects,
388 execution,
389 accessor,
390 });
391}
392
393fn access_key(subscript: Node<'_>, source: &str) -> PythonRuntimeAccessKey {
395 if let Some(literal) = python_plain_string_literal(subscript, source) {
396 return PythonRuntimeAccessKey::Property(literal.to_owned());
397 }
398 match subscript.kind() {
399 "integer" => crate::structural::PYTHON_STRUCTURAL_SPEC
400 .integer_literal_value(subscript, source)
401 .and_then(|value| value.decimal)
402 .and_then(|decimal| decimal.parse::<u128>().ok())
403 .map_or(PythonRuntimeAccessKey::Unsupported, |index| {
404 PythonRuntimeAccessKey::Index(index)
405 }),
406 "slice" | "tuple" | "list_splat" => PythonRuntimeAccessKey::Unsupported,
407 "string" | "concatenated_string" => PythonRuntimeAccessKey::Unsupported,
408 _ => PythonRuntimeAccessKey::Dynamic,
409 }
410}
411
412fn is_binding_target(node: Node<'_>) -> bool {
415 let mut current = node;
416 while let Some(parent) = current.parent() {
417 match parent.kind() {
418 "assignment" | "augmented_assignment" | "for_statement" | "for_in_clause" => {
419 return parent
420 .child_by_field_name("left")
421 .is_some_and(|left| left.id() == current.id());
422 }
423 "delete_statement" => return true,
424 "pattern_list" | "tuple_pattern" | "list_pattern" | "list_splat_pattern" => {
425 current = parent;
426 }
427 _ => return false,
428 }
429 }
430 false
431}
432
433pub(crate) fn enclosing_execution_context<'tree>(node: Node<'tree>) -> Option<Node<'tree>> {
436 let mut current = node;
437 while let Some(parent) = current.parent() {
438 if matches!(parent.kind(), "function_definition" | "lambda")
439 && parent
440 .child_by_field_name("body")
441 .is_some_and(|body| covers(body, current))
442 {
443 return Some(parent);
444 }
445 current = parent;
446 }
447 None
448}
449
450fn execution_context_accessor_coverage(
457 attribute: Node<'_>,
458 module_root: Node<'_>,
459 read_range: Range,
460 budget: &mut NodeBudget,
461) -> PythonRuntimeAccessorCoverage {
462 let scope = enclosing_execution_context(attribute)
463 .and_then(|function| function.child_by_field_name("body"))
464 .unwrap_or(module_root);
465 let mut stack = vec![scope];
466 while let Some(node) = stack.pop() {
467 if !budget.visit() {
468 return PythonRuntimeAccessorCoverage::UnknownAccessorEffects;
469 }
470 if matches!(node.kind(), "function_definition" | "lambda") {
471 let mut cursor = node.walk();
474 stack.extend(node.named_children(&mut cursor).filter(|child| {
475 Some(child.id()) != node.child_by_field_name("body").map(|body| body.id())
476 }));
477 continue;
478 }
479 if node.kind() == "call"
480 && python_hazard_can_precede_bounded(node_source_range(node), read_range, node, || {
481 budget.visit()
482 }) != Some(false)
483 {
484 return PythonRuntimeAccessorCoverage::UnknownAccessorEffects;
485 }
486 let mut cursor = node.walk();
487 stack.extend(node.named_children(&mut cursor));
488 }
489 PythonRuntimeAccessorCoverage::NoKnownAccessorEffects
490}
491
492pub(crate) fn python_hazard_can_precede_bounded(
502 hazard: Range,
503 read_range: Range,
504 hazard_node: Node<'_>,
505 mut scope_step: impl FnMut() -> bool,
506) -> Option<bool> {
507 if !scope_step() {
508 return None;
509 }
510 if hazard.end_byte <= read_range.start_byte {
511 return Some(true);
512 }
513 let mut current = hazard_node;
514 while let Some(parent) = current.parent() {
515 if !scope_step() {
516 return None;
517 }
518 if matches!(parent.kind(), "for_statement" | "while_statement")
519 && parent.start_byte() <= read_range.start_byte
520 && read_range.end_byte <= parent.end_byte()
521 {
522 return Some(true);
523 }
524 current = parent;
525 }
526 Some(false)
527}
528
529fn escaping_module_identifier(
535 node: Node<'_>,
536 bindings: &[ModuleBinding],
537 module_root: Node<'_>,
538 source: &str,
539 budget: &mut NodeBudget,
540) -> Option<String> {
541 let name = node_text(node, source);
542 let module = binding_root_module(node, name, bindings)?;
543 if root_resolution(node, &module, bindings, module_root, source, budget)
547 != PythonRuntimeRootResolution::ImportedModule
548 {
549 return None;
550 }
551 let parent = node.parent()?;
552 if parent.kind() == "attribute"
553 && parent
554 .child_by_field_name("object")
555 .is_some_and(|object| object.id() == node.id())
556 {
557 return None;
558 }
559 if matches!(
560 parent.kind(),
561 "import_statement" | "import_from_statement" | "aliased_import" | "dotted_name"
562 ) {
563 return None;
564 }
565 Some(module)
566}
567
568fn binding_root_module(
571 reference: Node<'_>,
572 name: &str,
573 bindings: &[ModuleBinding],
574) -> Option<String> {
575 let offset = reference.start_byte();
576 let mut module = None;
577 for binding in bindings {
578 if binding.local_name != name
579 || offset < binding.scope_start_byte
580 || offset > binding.scope_end_byte
581 {
582 continue;
583 }
584 match &module {
585 None => module = Some(binding.root_module.clone()),
586 Some(existing) if *existing == binding.root_module => {}
587 Some(_) => return None,
588 }
589 }
590 module.filter(|module| {
591 PROCESS_INPUT_CONTAINERS
592 .iter()
593 .any(|(reviewed, _)| reviewed == module)
594 })
595}
596
597fn root_resolution(
604 root: Node<'_>,
605 expected_module: &str,
606 bindings: &[ModuleBinding],
607 module_root: Node<'_>,
608 source: &str,
609 budget: &mut NodeBudget,
610) -> PythonRuntimeRootResolution {
611 let name = node_text(root, source);
612 match python_name_resolution_at(name, root, source) {
613 PythonLexicalNameResolution::Local | PythonLexicalNameResolution::Nonlocal => {
614 return if bindings
618 .iter()
619 .any(|binding| binding.local_name == name && binding.function_scoped)
620 {
621 PythonRuntimeRootResolution::Unknown
622 } else {
623 PythonRuntimeRootResolution::LexicallyBound
624 };
625 }
626 PythonLexicalNameResolution::Global | PythonLexicalNameResolution::Unbound => {}
627 }
628 match module_scope_binds_name_through_imports(module_root, name, source, budget) {
629 Some(true) => {}
630 Some(false) => return PythonRuntimeRootResolution::LexicallyBound,
631 None => return PythonRuntimeRootResolution::Unknown,
632 }
633 match binding_root_module(root, name, bindings) {
634 Some(module) if module == expected_module => PythonRuntimeRootResolution::ImportedModule,
635 Some(_) => PythonRuntimeRootResolution::LexicallyBound,
636 None => PythonRuntimeRootResolution::Unknown,
637 }
638}
639
640fn module_scope_binds_name_through_imports(
646 module_root: Node<'_>,
647 name: &str,
648 source: &str,
649 budget: &mut NodeBudget,
650) -> Option<bool> {
651 let mut imported = false;
652 let mut stack = vec![module_root];
653 while let Some(node) = stack.pop() {
654 if !budget.visit() {
655 return None;
656 }
657 if node.kind() == "wildcard_import" {
658 return None;
659 }
660 for binding in python_direct_scope_bindings_bounded(node, source, || budget.visit())? {
661 if node_text(binding.declaration, source) != name {
662 continue;
663 }
664 match binding.kind {
665 PythonDirectScopeBindingKind::Import => imported = true,
666 _ => return Some(false),
667 }
668 }
669 stack.extend(python_executable_scope_children(node));
672 }
673 Some(imported)
674}
675
676fn module_bindings(root: Node<'_>, source: &str) -> Vec<ModuleBinding> {
683 let mut bindings = Vec::new();
684 for binding in python_import_bindings_from_tree(root, source) {
685 let Some(root_module) = binding.qualified_name.split('.').next() else {
686 continue;
687 };
688 if !PROCESS_INPUT_CONTAINERS
689 .iter()
690 .any(|(reviewed, _)| *reviewed == root_module)
691 {
692 continue;
693 }
694 if binding.qualified_name != root_module && binding.consumed_attributes == 0 {
696 continue;
697 }
698 bindings.push(ModuleBinding {
699 local_name: binding.local_name.clone(),
700 root_module: root_module.to_owned(),
701 scope_start_byte: binding.scope_start_byte,
702 scope_end_byte: binding.scope_end_byte,
703 function_scoped: binding.is_function_scoped(),
704 });
705 }
706 bindings
707}
708
709use brokk_bifrost_core::analyzer::usages::reference_site::node_range as node_source_range;
710
711fn node_text<'a>(node: Node<'_>, source: &'a str) -> &'a str {
712 brokk_bifrost_core::analyzer::common::try_node_source_text(node, source)
713 .expect("a tree-sitter node range is valid UTF-8 source")
714}
715
716#[derive(Clone, Debug, Eq, PartialEq)]
718pub enum PythonModuleMemberMutation {
719 Write(Range),
721 Escape(Range),
724 Budget,
726}
727
728pub fn python_module_member_mutation(
756 root: Node<'_>,
757 source: &str,
758 source_module: &str,
759 module: &str,
760 member: &str,
761 max_nodes: usize,
762) -> Option<PythonModuleMemberMutation> {
763 let mut budget = NodeBudget::new(max_nodes);
764 let bindings = python_import_bindings_from_tree(root, source);
765 let Some(facts) =
766 python_sys_modules_aliases(root, source, source_module, module, &bindings, &mut budget)
767 else {
768 return Some(PythonModuleMemberMutation::Budget);
769 };
770 let table_aliases = &facts.aliases;
771 let locals: Vec<String> = bindings
772 .iter()
773 .filter(|binding| {
774 binding.qualified_name == module
775 || binding
776 .qualified_name
777 .split('.')
778 .next()
779 .is_some_and(|root_module| {
780 root_module == module && binding.consumed_attributes > 0
781 })
782 })
783 .map(|binding| binding.local_name.clone())
784 .collect();
785 let mut stack = vec![root];
786 while let Some(node) = stack.pop() {
787 if !budget.visit() {
788 return Some(PythonModuleMemberMutation::Budget);
789 }
790 if node.kind() == "call"
791 && python_mock_patch_may_replace_module_member(node, source, &bindings, module, member)
792 {
793 return Some(PythonModuleMemberMutation::Write(node_source_range(node)));
794 }
795 if node.kind() == "call"
798 && python_test_helper_call(node, source, &bindings)
799 == Some(PythonTestHelperCall::MockPatchDict)
800 && let Some(target) = python_mock_patch_dict_target(node, source)
801 && python_plain_string_literal(target, source) == Some("sys.modules")
802 {
803 let proof = PythonSysModulesProofContext {
804 source,
805 source_module,
806 module,
807 bindings: &bindings,
808 facts: &facts,
809 };
810 if python_mock_patch_dict_values(node, &proof, &mut budget)
811 != PythonSysModulesStoredValue::Unrelated
812 {
813 return Some(PythonModuleMemberMutation::Escape(node_source_range(
814 target,
815 )));
816 }
817 }
818 if node.kind() == "identifier" {
819 let name = node_text(node, source);
820 if let Some(table) = python_sys_modules_table(node, source) {
821 if python_imported_path(table, source, &bindings)
822 .is_some_and(|path| path == ["sys", "modules"])
823 {
824 match python_sys_modules_lookup(
825 table,
826 source,
827 source_module,
828 module,
829 &bindings,
830 &facts,
831 &mut budget,
832 ) {
833 PythonSysModulesLookup::Unrelated => {}
834 PythonSysModulesLookup::Escape(range) => {
835 return Some(PythonModuleMemberMutation::Escape(range));
836 }
837 PythonSysModulesLookup::Target(read) => {
838 if let Some(mutation) =
839 python_sys_modules_read_use(read, source, member, table_aliases)
840 {
841 return Some(mutation);
842 }
843 }
844 }
845 } else {
846 return Some(PythonModuleMemberMutation::Escape(node_source_range(node)));
847 }
848 }
849 let table_alias = table_aliases.contains(name);
850 if table_alias && python_module_table_alias_binding_use(node, source, table_aliases) {
851 } else if (table_alias
853 || (locals.iter().any(|local| local == name)
854 && !matches!(
855 python_name_resolution_at(name, node, source),
856 PythonLexicalNameResolution::Local | PythonLexicalNameResolution::Nonlocal
857 )))
858 && let Some(mutation) =
859 module_binding_use(node, source, &bindings, table_aliases, module, member)
860 {
861 return Some(mutation);
862 }
863 }
864 let mut cursor = node.walk();
865 stack.extend(node.named_children(&mut cursor));
866 }
867 None
868}
869
870enum PythonSysModulesLookup<'tree> {
871 Unrelated,
872 Target(Node<'tree>),
873 Escape(Range),
874}
875
876fn python_sys_modules_table<'tree>(node: Node<'tree>, source: &str) -> Option<Node<'tree>> {
878 let table = node.parent()?;
879 (table.kind() == "attribute"
880 && table
881 .child_by_field_name("object")
882 .is_some_and(|object| object.id() == node.id())
883 && table
884 .child_by_field_name("attribute")
885 .is_some_and(|attribute| node_text(attribute, source) == "modules"))
886 .then_some(table)
887}
888
889fn python_sys_modules_read_table<'tree>(read: Node<'tree>, source: &str) -> Option<Node<'tree>> {
890 if read.kind() == "subscript" {
891 let table = read.child_by_field_name("value")?;
892 return (table.kind() == "attribute"
893 && table
894 .child_by_field_name("attribute")
895 .is_some_and(|attribute| node_text(attribute, source) == "modules"))
896 .then_some(table);
897 }
898 if read.kind() == "call" {
899 let function = read.child_by_field_name("function")?;
900 if function.kind() != "attribute"
901 || function
902 .child_by_field_name("attribute")
903 .is_none_or(|attribute| {
904 !matches!(node_text(attribute, source), "get" | "pop" | "setdefault")
905 })
906 {
907 return None;
908 }
909 let table = function.child_by_field_name("object")?;
910 return (table.kind() == "attribute"
911 && table
912 .child_by_field_name("attribute")
913 .is_some_and(|attribute| node_text(attribute, source) == "modules"))
914 .then_some(table);
915 }
916 None
917}
918
919fn python_sys_modules_lookup<'tree>(
920 table: Node<'tree>,
921 source: &str,
922 source_module: &str,
923 module: &str,
924 bindings: &[crate::imports::PythonImportBinding],
925 facts: &PythonSysModulesFacts<'tree>,
926 budget: &mut NodeBudget,
927) -> PythonSysModulesLookup<'tree> {
928 let escaped = || PythonSysModulesLookup::Escape(node_source_range(table));
932 if python_imported_path(table, source, bindings).is_none_or(|path| path != ["sys", "modules"]) {
933 return escaped();
934 }
935 let Some(parent) = table.parent() else {
936 return escaped();
937 };
938 let proof = PythonSysModulesProofContext {
939 source,
940 source_module,
941 module,
942 bindings,
943 facts,
944 };
945 if parent.kind() == "subscript"
946 && parent
947 .child_by_field_name("value")
948 .is_some_and(|value| value.id() == table.id())
949 {
950 if is_binding_target(parent) {
951 if parent
952 .parent()
953 .is_some_and(|parent| parent.kind() == "delete_statement")
954 {
955 return PythonSysModulesLookup::Unrelated;
956 }
957 let Some(value) = parent
958 .parent()
959 .filter(|assignment| assignment.kind() == "assignment")
960 .and_then(|assignment| assignment.child_by_field_name("right"))
961 else {
962 return escaped();
963 };
964 return match python_sys_modules_stored_value(
965 value,
966 &proof,
967 budget,
968 &mut HashSet::default(),
969 ) {
970 PythonSysModulesStoredValue::Unrelated => PythonSysModulesLookup::Unrelated,
971 PythonSysModulesStoredValue::Target | PythonSysModulesStoredValue::Unknown => {
972 escaped()
973 }
974 };
975 }
976 return python_sys_modules_read_identity(
977 parent.child_by_field_name("subscript"),
978 parent,
979 &proof,
980 budget,
981 );
982 }
983 if parent.kind() == "attribute"
984 && parent
985 .child_by_field_name("object")
986 .is_some_and(|object| object.id() == table.id())
987 {
988 let Some(call) = parent.parent().filter(|call| {
989 call.kind() == "call"
990 && call
991 .child_by_field_name("function")
992 .is_some_and(|function| function.id() == parent.id())
993 }) else {
994 return escaped();
995 };
996 let method = parent
997 .child_by_field_name("attribute")
998 .map(|attribute| node_text(attribute, source));
999 match method {
1000 Some("get" | "pop") => {
1001 let args = python_sys_modules_arguments(call);
1002 return python_sys_modules_read_identity(
1003 args.first().copied(),
1004 call,
1005 &proof,
1006 budget,
1007 );
1008 }
1009 Some("setdefault") => {
1010 let args = python_sys_modules_arguments(call);
1011 let default = args
1012 .get(1)
1013 .copied()
1014 .or_else(|| python_sys_modules_keyword_argument(call, source, "default"));
1015 if let Some(default) = default
1016 && python_sys_modules_stored_value(
1017 default,
1018 &proof,
1019 budget,
1020 &mut HashSet::default(),
1021 ) != PythonSysModulesStoredValue::Unrelated
1022 {
1023 return escaped();
1024 }
1025 return python_sys_modules_read_identity(
1026 args.first().copied(),
1027 call,
1028 &proof,
1029 budget,
1030 );
1031 }
1032 Some("update") => {
1033 let Some(arguments) = call.child_by_field_name("arguments") else {
1034 return escaped();
1035 };
1036 let mut cursor = arguments.walk();
1037 for argument in arguments.named_children(&mut cursor) {
1038 if python_sys_modules_update_argument(argument, &proof, budget)
1039 != PythonSysModulesStoredValue::Unrelated
1040 {
1041 return escaped();
1042 }
1043 }
1044 return PythonSysModulesLookup::Unrelated;
1045 }
1046 Some("clear" | "keys") => return PythonSysModulesLookup::Unrelated,
1047 _ => return escaped(),
1048 }
1049 }
1050 let argument_list = if parent.kind() == "keyword_argument" {
1053 parent.parent()
1054 } else {
1055 Some(parent)
1056 };
1057 if let Some(call) = argument_list
1058 .filter(|arguments| arguments.kind() == "argument_list")
1059 .and_then(|arguments| arguments.parent())
1060 .filter(|call| {
1061 python_test_helper_call(*call, source, bindings)
1062 == Some(PythonTestHelperCall::MockPatchDict)
1063 })
1064 && python_mock_patch_dict_target(call, source)
1065 .is_some_and(|target| target.id() == table.id())
1066 {
1067 return match python_mock_patch_dict_values(call, &proof, budget) {
1068 PythonSysModulesStoredValue::Unrelated => PythonSysModulesLookup::Unrelated,
1069 PythonSysModulesStoredValue::Target | PythonSysModulesStoredValue::Unknown => escaped(),
1070 };
1071 }
1072 if parent.kind() == "argument_list"
1073 && let Some(call) = parent.parent().filter(|call| call.kind() == "call")
1074 && python_sys_modules_arguments(call)
1075 .first()
1076 .is_some_and(|first| first.id() == table.id())
1077 {
1078 match python_test_helper_call(call, source, bindings) {
1079 Some(PythonTestHelperCall::MonkeyPatchSetItem(receiver))
1080 if python_monkeypatch_receiver(receiver, &proof) =>
1081 {
1082 let value = python_sys_modules_arguments(call)
1083 .get(2)
1084 .copied()
1085 .or_else(|| python_sys_modules_keyword_argument(call, source, "value"));
1086 return match value.map(|value| {
1087 python_sys_modules_stored_value(value, &proof, budget, &mut HashSet::default())
1088 }) {
1089 Some(PythonSysModulesStoredValue::Unrelated) => {
1090 PythonSysModulesLookup::Unrelated
1091 }
1092 _ => escaped(),
1093 };
1094 }
1095 Some(PythonTestHelperCall::MonkeyPatchDelItem(receiver))
1096 if python_monkeypatch_receiver(receiver, &proof) =>
1097 {
1098 return PythonSysModulesLookup::Unrelated;
1099 }
1100 _ => {}
1101 }
1102 }
1103 escaped()
1104}
1105
1106fn python_mock_patch_dict_target<'tree>(call: Node<'tree>, source: &str) -> Option<Node<'tree>> {
1109 match python_sys_modules_arguments(call).first() {
1110 Some(first) if first.kind() == "list_splat" => None,
1111 Some(first) => Some(*first),
1112 None => python_sys_modules_keyword_argument(call, source, "in_dict"),
1113 }
1114}
1115
1116fn python_mock_patch_dict_values<'tree>(
1120 call: Node<'tree>,
1121 proof: &PythonSysModulesProofContext<'_, 'tree>,
1122 budget: &mut NodeBudget,
1123) -> PythonSysModulesStoredValue {
1124 let source = proof.source;
1125 let Some(arguments) = call.child_by_field_name("arguments") else {
1126 return PythonSysModulesStoredValue::Unknown;
1127 };
1128 let target = python_sys_modules_arguments(call).first().map(Node::id);
1129 let mut cursor = arguments.walk();
1130 for argument in arguments.named_children(&mut cursor) {
1131 let keyword = (argument.kind() == "keyword_argument")
1132 .then(|| argument.child_by_field_name("name"))
1133 .flatten()
1134 .map(|name| node_text(name, source));
1135 let identity = if Some(argument.id()) == target
1136 || matches!(keyword, Some("in_dict" | "clear"))
1137 {
1138 PythonSysModulesStoredValue::Unrelated
1139 } else if keyword == Some("values") {
1140 argument
1141 .child_by_field_name("value")
1142 .map(|value| {
1143 python_sys_modules_update_values(value, proof, budget, &mut HashSet::default())
1144 })
1145 .unwrap_or(PythonSysModulesStoredValue::Unknown)
1146 } else {
1147 python_sys_modules_update_argument(argument, proof, budget)
1148 };
1149 if identity != PythonSysModulesStoredValue::Unrelated {
1150 return identity;
1151 }
1152 }
1153 PythonSysModulesStoredValue::Unrelated
1154}
1155
1156fn python_sys_modules_update_argument<'tree>(
1159 argument: Node<'tree>,
1160 proof: &PythonSysModulesProofContext<'_, 'tree>,
1161 budget: &mut NodeBudget,
1162) -> PythonSysModulesStoredValue {
1163 if argument.kind() == "keyword_argument" {
1164 argument
1165 .child_by_field_name("value")
1166 .map(|value| {
1167 python_sys_modules_stored_value(value, proof, budget, &mut HashSet::default())
1168 })
1169 .unwrap_or(PythonSysModulesStoredValue::Unknown)
1170 } else if matches!(argument.kind(), "dictionary_splat" | "list_splat") {
1171 PythonSysModulesStoredValue::Unknown
1172 } else {
1173 python_sys_modules_update_values(argument, proof, budget, &mut HashSet::default())
1174 }
1175}
1176
1177fn python_monkeypatch_receiver(
1186 receiver: Node<'_>,
1187 proof: &PythonSysModulesProofContext<'_, '_>,
1188) -> bool {
1189 let source = proof.source;
1190 let is_monkeypatch_class = |expression: Option<Node<'_>>| {
1191 expression
1192 .and_then(|expression| python_imported_path(expression, source, proof.bindings))
1193 .is_some_and(|path| path.last().is_some_and(|name| name == "MonkeyPatch"))
1194 };
1195 if receiver.kind() != "identifier" {
1196 return false;
1197 }
1198 let name = node_text(receiver, source);
1199 let scope = python_alias_scope(receiver);
1200 if scope.kind() != "function_definition"
1201 || !python_sys_modules_has_one_binding(proof, scope, name)
1202 {
1203 return false;
1204 }
1205 if let Some(parameters) = scope.child_by_field_name("parameters") {
1206 let mut cursor = parameters.walk();
1207 if let Some(parameter) = parameters
1208 .named_children(&mut cursor)
1209 .find(|parameter| python_parameter_name(*parameter, source).as_deref() == Some(name))
1210 {
1211 return name == "monkeypatch"
1212 || is_monkeypatch_class(
1213 parameter
1214 .child_by_field_name("type")
1215 .and_then(|annotation| annotation.named_child(0)),
1216 );
1217 }
1218 }
1219 let key = (scope.start_byte(), scope.end_byte(), name.to_owned());
1220 if let Some([manager]) = proof.facts.context_managers.get(&key).map(Vec::as_slice) {
1221 return manager.kind() == "call"
1223 && manager
1224 .child_by_field_name("function")
1225 .filter(|function| function.kind() == "attribute")
1226 .is_some_and(|function| {
1227 function
1228 .child_by_field_name("attribute")
1229 .is_some_and(|attribute| node_text(attribute, source) == "context")
1230 && is_monkeypatch_class(function.child_by_field_name("object"))
1231 });
1232 }
1233 proof
1234 .facts
1235 .assignments
1236 .get(&key)
1237 .is_some_and(|values| match values.as_slice() {
1238 [value] => {
1239 value.kind() == "call"
1240 && is_monkeypatch_class(value.child_by_field_name("function"))
1241 }
1242 _ => false,
1243 })
1244}
1245
1246fn python_sys_modules_arguments(call: Node<'_>) -> Vec<Node<'_>> {
1247 let Some(arguments) = call.child_by_field_name("arguments") else {
1248 return Vec::new();
1249 };
1250 let mut cursor = arguments.walk();
1251 arguments
1252 .named_children(&mut cursor)
1253 .filter(|argument| argument.kind() != "keyword_argument")
1254 .collect()
1255}
1256
1257fn python_sys_modules_keyword_argument<'tree>(
1258 call: Node<'tree>,
1259 source: &str,
1260 name: &str,
1261) -> Option<Node<'tree>> {
1262 let arguments = call.child_by_field_name("arguments")?;
1263 let mut cursor = arguments.walk();
1264 arguments
1265 .named_children(&mut cursor)
1266 .find(|argument| {
1267 argument.kind() == "keyword_argument"
1268 && argument
1269 .child_by_field_name("name")
1270 .is_some_and(|key| node_text(key, source) == name)
1271 })
1272 .and_then(|argument| argument.child_by_field_name("value"))
1273}
1274
1275fn python_sys_modules_read_identity<'tree>(
1276 key: Option<Node<'tree>>,
1277 read: Node<'tree>,
1278 proof: &PythonSysModulesProofContext<'_, 'tree>,
1279 budget: &mut NodeBudget,
1280) -> PythonSysModulesLookup<'tree> {
1281 let Some(key) = key else {
1282 return PythonSysModulesLookup::Escape(node_source_range(read));
1283 };
1284 if key.kind() == "identifier" && node_text(key, proof.source) == "__name__" {
1285 return if proof.source_module == proof.module {
1286 PythonSysModulesLookup::Target(read)
1287 } else {
1288 PythonSysModulesLookup::Unrelated
1289 };
1290 }
1291 let Some(key_value) = crate::decode_python_string_value(key, proof.source).ok() else {
1292 if python_sys_modules_guard_excludes_module(key, read, proof, budget) {
1293 return PythonSysModulesLookup::Unrelated;
1294 }
1295 return PythonSysModulesLookup::Target(read);
1296 };
1297 if key_value == proof.module {
1298 PythonSysModulesLookup::Target(read)
1299 } else {
1300 PythonSysModulesLookup::Unrelated
1301 }
1302}
1303
1304#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1305enum PythonGuardTruth {
1306 True,
1307 False,
1308 Unknown,
1309}
1310
1311fn python_sys_modules_guard_excludes_module(
1314 key: Node<'_>,
1315 read: Node<'_>,
1316 proof: &PythonSysModulesProofContext<'_, '_>,
1317 budget: &mut NodeBudget,
1318) -> bool {
1319 if key.kind() != "identifier" {
1320 return false;
1321 }
1322 let key_name = node_text(key, proof.source);
1323 let mut child = read;
1324 let mut ancestor = read.parent();
1325 while let Some(node) = ancestor {
1326 if !budget.visit() {
1327 return false;
1328 }
1329 if matches!(node.kind(), "if_statement" | "elif_clause")
1330 && let (Some(condition), Some(consequence)) = (
1331 node.child_by_field_name("condition"),
1332 node.child_by_field_name("consequence"),
1333 )
1334 && consequence.start_byte() <= read.start_byte()
1335 && read.end_byte() <= consequence.end_byte()
1336 && python_sys_modules_key_binding_unchanged(key, condition, read, proof, budget)
1337 && python_sys_modules_guard_truth(
1338 condition,
1339 key_name,
1340 proof.module,
1341 proof.source,
1342 budget,
1343 ) == PythonGuardTruth::False
1344 {
1345 return true;
1346 }
1347 if node.kind() == "block"
1348 && let Some(statement) = python_sys_modules_direct_child_containing(node, child)
1349 && python_sys_modules_prior_continue_guard_excludes(
1350 node, statement, key, read, key_name, proof, budget,
1351 )
1352 {
1353 return true;
1354 }
1355 child = node;
1356 ancestor = node.parent();
1357 }
1358 false
1359}
1360
1361fn python_sys_modules_key_binding_unchanged(
1366 key: Node<'_>,
1367 guard: Node<'_>,
1368 read: Node<'_>,
1369 proof: &PythonSysModulesProofContext<'_, '_>,
1370 budget: &mut NodeBudget,
1371) -> bool {
1372 let name = node_text(key, proof.source);
1373 let guard_reference = python_sys_modules_guard_key_reference(guard, name, proof.source, budget);
1374 let Some(guard_reference) = guard_reference else {
1375 return false;
1376 };
1377 let scope = python_alias_scope(key);
1378 let key_resolution = python_name_resolution_at(name, key, proof.source);
1379 let guard_resolution = python_name_resolution_at(name, guard_reference, proof.source);
1380 let same_lexical_binding = key_resolution == guard_resolution
1381 && (key_resolution == PythonLexicalNameResolution::Local
1382 || (scope.kind() == "module"
1383 && key_resolution == PythonLexicalNameResolution::Unbound));
1384 if python_alias_scope(guard_reference).id() != scope.id()
1385 || python_alias_scope(read).id() != scope.id()
1386 || !same_lexical_binding
1387 {
1388 return false;
1389 }
1390 if !python_sys_modules_has_one_binding(proof, scope, name) {
1391 return false;
1392 }
1393 let start = guard.start_byte();
1394 let guard_end = guard.end_byte();
1395 let end = read.start_byte();
1396 let mut stack = vec![scope.child_by_field_name("body").unwrap_or(scope)];
1397 while let Some(node) = stack.pop() {
1398 if !budget.visit() {
1399 return false;
1400 }
1401 if node.start_byte() >= end || node.end_byte() <= start {
1402 continue;
1403 }
1404 if python_direct_scope_bindings_bounded(node, proof.source, || budget.visit()).is_none_or(
1405 |bindings| {
1406 bindings.into_iter().any(|binding| {
1407 node_text(binding.declaration, proof.source) == name
1408 && start <= binding.declaration.start_byte()
1409 && binding.declaration.start_byte() < end
1410 })
1411 },
1412 ) {
1413 return false;
1414 }
1415 let mut cursor = node.walk();
1416 for child in node.named_children(&mut cursor) {
1417 if matches!(child.kind(), "call")
1418 && guard_end <= child.start_byte()
1419 && child.end_byte() < end
1420 {
1421 return false;
1422 }
1423 }
1424 stack.extend(python_executable_scope_children(node));
1425 }
1426 true
1427}
1428
1429fn python_sys_modules_guard_key_reference<'tree>(
1430 condition: Node<'tree>,
1431 name: &str,
1432 source: &str,
1433 budget: &mut NodeBudget,
1434) -> Option<Node<'tree>> {
1435 let mut stack = vec![condition];
1436 while let Some(node) = stack.pop() {
1437 if !budget.visit() {
1438 return None;
1439 }
1440 if node.kind() == "identifier" && node_text(node, source) == name {
1441 return Some(node);
1442 }
1443 let mut cursor = node.walk();
1444 stack.extend(node.named_children(&mut cursor));
1445 }
1446 None
1447}
1448
1449fn python_sys_modules_direct_child_containing<'tree>(
1450 block: Node<'tree>,
1451 child: Node<'tree>,
1452) -> Option<Node<'tree>> {
1453 let mut direct = child;
1454 while direct
1455 .parent()
1456 .is_some_and(|parent| parent.id() != block.id())
1457 {
1458 direct = direct.parent()?;
1459 }
1460 direct
1461 .parent()
1462 .is_some_and(|parent| parent.id() == block.id())
1463 .then_some(direct)
1464}
1465
1466fn python_sys_modules_prior_continue_guard_excludes(
1467 block: Node<'_>,
1468 read_statement: Node<'_>,
1469 key: Node<'_>,
1470 read: Node<'_>,
1471 key_name: &str,
1472 proof: &PythonSysModulesProofContext<'_, '_>,
1473 budget: &mut NodeBudget,
1474) -> bool {
1475 let mut cursor = block.walk();
1476 for statement in block.named_children(&mut cursor) {
1477 if statement.id() == read_statement.id() {
1478 break;
1479 }
1480 if statement.kind() != "if_statement" {
1481 continue;
1482 }
1483 let Some(condition) = statement.child_by_field_name("condition") else {
1484 continue;
1485 };
1486 if condition.kind() != "not_operator"
1487 || !python_sys_modules_guard_has_direct_abrupt_consequence(statement)
1488 || !python_sys_modules_key_binding_unchanged(key, condition, read, proof, budget)
1489 {
1490 continue;
1491 }
1492 let Some(guard_condition) = condition.named_child(0) else {
1493 continue;
1494 };
1495 if python_sys_modules_guard_truth(
1496 guard_condition,
1497 key_name,
1498 proof.module,
1499 proof.source,
1500 budget,
1501 ) == PythonGuardTruth::False
1502 {
1503 return true;
1504 }
1505 }
1506 false
1507}
1508
1509fn python_sys_modules_guard_has_direct_abrupt_consequence(statement: Node<'_>) -> bool {
1510 let Some(consequence) = statement.child_by_field_name("consequence") else {
1511 return false;
1512 };
1513 let mut cursor = consequence.walk();
1514 let mut statements = consequence.named_children(&mut cursor);
1515 let Some(only) = statements.next() else {
1516 return false;
1517 };
1518 statements.next().is_none()
1519 && matches!(
1520 only.kind(),
1521 "break_statement" | "continue_statement" | "return_statement" | "raise_statement"
1522 )
1523}
1524
1525fn python_sys_modules_guard_truth(
1526 condition: Node<'_>,
1527 key_name: &str,
1528 module: &str,
1529 source: &str,
1530 budget: &mut NodeBudget,
1531) -> PythonGuardTruth {
1532 let mut pending = vec![(condition, false)];
1533 let mut results = HashMap::<usize, PythonGuardTruth>::default();
1534 while let Some((node, visited)) = pending.pop() {
1535 if !budget.visit() {
1536 return PythonGuardTruth::Unknown;
1537 }
1538 if !visited
1539 && matches!(
1540 node.kind(),
1541 "boolean_operator" | "not_operator" | "parenthesized_expression"
1542 )
1543 {
1544 pending.push((node, true));
1545 let mut cursor = node.walk();
1546 for child in node.named_children(&mut cursor) {
1547 pending.push((child, false));
1548 }
1549 continue;
1550 }
1551 let value = match node.kind() {
1552 "boolean_operator" => {
1553 let Some(left) = node.child_by_field_name("left") else {
1554 return PythonGuardTruth::Unknown;
1555 };
1556 let Some(right) = node.child_by_field_name("right") else {
1557 return PythonGuardTruth::Unknown;
1558 };
1559 let Some(operator) = node.child_by_field_name("operator") else {
1560 return PythonGuardTruth::Unknown;
1561 };
1562 let (Some(left), Some(right)) = (results.get(&left.id()), results.get(&right.id()))
1563 else {
1564 return PythonGuardTruth::Unknown;
1565 };
1566 match node_text(operator, source) {
1567 "and" => python_guard_and(*left, *right),
1568 "or" => python_guard_or(*left, *right),
1569 _ => PythonGuardTruth::Unknown,
1570 }
1571 }
1572 "not_operator" => node
1573 .named_child(0)
1574 .and_then(|child| results.get(&child.id()).copied())
1575 .map(python_guard_not)
1576 .unwrap_or(PythonGuardTruth::Unknown),
1577 "parenthesized_expression" => node
1578 .named_child(0)
1579 .and_then(|child| results.get(&child.id()).copied())
1580 .unwrap_or(PythonGuardTruth::Unknown),
1581 _ => python_sys_modules_guard_atom(node, key_name, module, source),
1582 };
1583 results.insert(node.id(), value);
1584 }
1585 results
1586 .get(&condition.id())
1587 .copied()
1588 .unwrap_or(PythonGuardTruth::Unknown)
1589}
1590
1591fn python_guard_and(left: PythonGuardTruth, right: PythonGuardTruth) -> PythonGuardTruth {
1592 match (left, right) {
1593 (PythonGuardTruth::False, _) | (_, PythonGuardTruth::False) => PythonGuardTruth::False,
1594 (PythonGuardTruth::True, PythonGuardTruth::True) => PythonGuardTruth::True,
1595 _ => PythonGuardTruth::Unknown,
1596 }
1597}
1598
1599fn python_guard_or(left: PythonGuardTruth, right: PythonGuardTruth) -> PythonGuardTruth {
1600 match (left, right) {
1601 (PythonGuardTruth::True, _) | (_, PythonGuardTruth::True) => PythonGuardTruth::True,
1602 (PythonGuardTruth::False, PythonGuardTruth::False) => PythonGuardTruth::False,
1603 _ => PythonGuardTruth::Unknown,
1604 }
1605}
1606
1607fn python_guard_not(value: PythonGuardTruth) -> PythonGuardTruth {
1608 match value {
1609 PythonGuardTruth::True => PythonGuardTruth::False,
1610 PythonGuardTruth::False => PythonGuardTruth::True,
1611 PythonGuardTruth::Unknown => PythonGuardTruth::Unknown,
1612 }
1613}
1614
1615fn python_sys_modules_guard_atom(
1616 node: Node<'_>,
1617 key_name: &str,
1618 module: &str,
1619 source: &str,
1620) -> PythonGuardTruth {
1621 if node.kind() == "comparison_operator" {
1622 return python_sys_modules_guard_comparison(node, key_name, module, source);
1623 }
1624 if node.kind() == "call" {
1625 return python_sys_modules_guard_string_method(node, key_name, module, source);
1626 }
1627 PythonGuardTruth::Unknown
1628}
1629
1630fn python_sys_modules_guard_comparison(
1631 node: Node<'_>,
1632 key_name: &str,
1633 module: &str,
1634 source: &str,
1635) -> PythonGuardTruth {
1636 let mut cursor = node.walk();
1637 let operands = node.named_children(&mut cursor).collect::<Vec<_>>();
1638 let mut cursor = node.walk();
1639 let operators = node
1640 .children_by_field_name("operators", &mut cursor)
1641 .collect::<Vec<_>>();
1642 let ([left, right], [operator]) = (operands.as_slice(), operators.as_slice()) else {
1643 return PythonGuardTruth::Unknown;
1644 };
1645 let operator = node_text(*operator, source);
1646 let key_left = left.kind() == "identifier" && node_text(*left, source) == key_name;
1647 let key_right = right.kind() == "identifier" && node_text(*right, source) == key_name;
1648 if key_left || key_right {
1649 let literal = if key_left {
1650 crate::decode_python_string_value(*right, source)
1651 } else {
1652 crate::decode_python_string_value(*left, source)
1653 };
1654 if matches!(operator, "==" | "!=")
1655 && let Ok(literal) = literal
1656 {
1657 let matches = literal == module;
1658 return python_guard_bool(if operator == "==" { matches } else { !matches });
1659 }
1660 if operator == "in" && key_left {
1661 return python_sys_modules_literal_membership(*right, module, source);
1662 }
1663 if operator == "in"
1664 && key_right
1665 && let Ok(literal) = literal
1666 {
1667 return python_guard_bool(module.contains(&literal));
1668 }
1669 }
1670 PythonGuardTruth::Unknown
1671}
1672
1673fn python_sys_modules_literal_membership(
1674 collection: Node<'_>,
1675 module: &str,
1676 source: &str,
1677) -> PythonGuardTruth {
1678 if !matches!(collection.kind(), "tuple" | "list" | "set") {
1679 return PythonGuardTruth::Unknown;
1680 }
1681 let mut cursor = collection.walk();
1682 let mut contains = false;
1683 for value in collection.named_children(&mut cursor) {
1684 let Ok(literal) = crate::decode_python_string_value(value, source) else {
1685 return PythonGuardTruth::Unknown;
1686 };
1687 contains |= literal == module;
1688 }
1689 python_guard_bool(contains)
1690}
1691
1692fn python_sys_modules_guard_string_method(
1693 call: Node<'_>,
1694 key_name: &str,
1695 module: &str,
1696 source: &str,
1697) -> PythonGuardTruth {
1698 let Some(function) = call.child_by_field_name("function") else {
1699 return PythonGuardTruth::Unknown;
1700 };
1701 if function.kind() != "attribute"
1702 || !function
1703 .child_by_field_name("object")
1704 .is_some_and(|object| {
1705 object.kind() == "identifier" && node_text(object, source) == key_name
1706 })
1707 {
1708 return PythonGuardTruth::Unknown;
1709 }
1710 let Some(method) = function
1711 .child_by_field_name("attribute")
1712 .map(|name| node_text(name, source))
1713 else {
1714 return PythonGuardTruth::Unknown;
1715 };
1716 let Some(arguments) = call.child_by_field_name("arguments") else {
1717 return PythonGuardTruth::Unknown;
1718 };
1719 let mut cursor = arguments.walk();
1720 let mut values = arguments.named_children(&mut cursor);
1721 let Some(first) = values.next() else {
1722 return PythonGuardTruth::Unknown;
1723 };
1724 if values.next().is_some() || first.kind() == "keyword_argument" {
1725 return PythonGuardTruth::Unknown;
1726 }
1727 let matches = if let Ok(literal) = crate::decode_python_string_value(first, source) {
1728 match method {
1729 "startswith" => module.starts_with(&literal),
1730 "endswith" => module.ends_with(&literal),
1731 _ => return PythonGuardTruth::Unknown,
1732 }
1733 } else if matches!(first.kind(), "tuple" | "list" | "set") {
1734 let mut cursor = first.walk();
1735 let mut matched = false;
1736 for value in first.named_children(&mut cursor) {
1737 let Ok(literal) = crate::decode_python_string_value(value, source) else {
1738 return PythonGuardTruth::Unknown;
1739 };
1740 matched |= match method {
1741 "startswith" => module.starts_with(&literal),
1742 "endswith" => module.ends_with(&literal),
1743 _ => return PythonGuardTruth::Unknown,
1744 };
1745 }
1746 matched
1747 } else {
1748 return PythonGuardTruth::Unknown;
1749 };
1750 python_guard_bool(matches)
1751}
1752
1753fn python_guard_bool(value: bool) -> PythonGuardTruth {
1754 if value {
1755 PythonGuardTruth::True
1756 } else {
1757 PythonGuardTruth::False
1758 }
1759}
1760
1761#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1762enum PythonSysModulesStoredValue {
1763 Target,
1764 Unrelated,
1765 Unknown,
1766}
1767
1768struct PythonSysModulesFacts<'tree> {
1769 aliases: HashSet<String>,
1770 assignments: HashMap<(usize, usize, String), Vec<Node<'tree>>>,
1771 binding_counts: HashMap<(usize, usize, String), usize>,
1772 context_managers: HashMap<(usize, usize, String), Vec<Node<'tree>>>,
1774 classes: Vec<PythonExternalClassAlias>,
1775}
1776
1777struct PythonSysModulesProofContext<'a, 'tree> {
1778 source: &'a str,
1779 source_module: &'a str,
1780 module: &'a str,
1781 bindings: &'a [crate::imports::PythonImportBinding],
1782 facts: &'a PythonSysModulesFacts<'tree>,
1783}
1784
1785fn python_sys_modules_has_one_binding(
1786 proof: &PythonSysModulesProofContext<'_, '_>,
1787 scope: Node<'_>,
1788 name: &str,
1789) -> bool {
1790 let count = proof
1791 .facts
1792 .binding_counts
1793 .get(&(scope.start_byte(), scope.end_byte(), name.to_owned()))
1794 .copied()
1795 .unwrap_or_default()
1796 + proof
1797 .bindings
1798 .iter()
1799 .filter(|binding| {
1800 binding.local_name == name
1801 && binding.scope_start_byte == scope.start_byte()
1802 && binding.scope_end_byte == scope.end_byte()
1803 })
1804 .count();
1805 count == 1
1806}
1807
1808fn python_sys_modules_value_scope<'tree>(node: Node<'tree>, source: &str) -> Node<'tree> {
1809 let nearest = python_alias_scope(node);
1810 let name = python_static_path(node, source)
1811 .and_then(|(root, _)| (root.kind() == "identifier").then_some(node_text(root, source)))
1812 .unwrap_or_default();
1813 match python_name_resolution_at(name, node, source) {
1814 PythonLexicalNameResolution::Local | PythonLexicalNameResolution::Nonlocal => nearest,
1815 PythonLexicalNameResolution::Global => {
1816 let mut scope = nearest;
1817 while scope.kind() != "module" {
1818 scope = python_alias_scope(scope.parent().expect("lexical scope has a parent"));
1819 }
1820 scope
1821 }
1822 PythonLexicalNameResolution::Unbound if nearest.kind() == "class_definition" => nearest,
1823 PythonLexicalNameResolution::Unbound => {
1824 let mut scope = nearest;
1825 while scope.kind() != "module" {
1826 scope = python_alias_scope(scope.parent().expect("lexical scope has a parent"));
1827 }
1828 scope
1829 }
1830 }
1831}
1832
1833fn python_sys_modules_stored_value<'tree>(
1834 value: Node<'tree>,
1835 context: &PythonSysModulesProofContext<'_, 'tree>,
1836 budget: &mut NodeBudget,
1837 seen: &mut HashSet<(usize, usize, String)>,
1838) -> PythonSysModulesStoredValue {
1839 let source = context.source;
1840 let module = context.module;
1841 let bindings = context.bindings;
1842 let facts = context.facts;
1843 if !budget.visit() {
1844 return PythonSysModulesStoredValue::Unknown;
1845 }
1846 if python_sys_modules_table(value, source).is_some() {
1847 return PythonSysModulesStoredValue::Unknown;
1848 }
1849 if let Some(table) = python_sys_modules_read_table(value, source) {
1850 if python_imported_path(table, source, bindings)
1851 .is_none_or(|path| path != ["sys", "modules"])
1852 {
1853 return PythonSysModulesStoredValue::Unknown;
1854 }
1855 return match python_sys_modules_read_identity(
1856 if value.kind() == "subscript" {
1857 value.child_by_field_name("subscript")
1858 } else {
1859 python_sys_modules_arguments(value).first().copied()
1860 },
1861 value,
1862 context,
1863 budget,
1864 ) {
1865 PythonSysModulesLookup::Target(_) => PythonSysModulesStoredValue::Target,
1866 PythonSysModulesLookup::Unrelated => PythonSysModulesStoredValue::Unrelated,
1867 PythonSysModulesLookup::Escape(_) => PythonSysModulesStoredValue::Unknown,
1868 };
1869 }
1870 if matches!(
1871 value.kind(),
1872 "string"
1873 | "concatenated_string"
1874 | "integer"
1875 | "float"
1876 | "true"
1877 | "false"
1878 | "none"
1879 | "list"
1880 | "tuple"
1881 | "set"
1882 | "dictionary"
1883 | "list_comprehension"
1884 | "set_comprehension"
1885 | "dictionary_comprehension"
1886 | "generator_expression"
1887 ) {
1888 return PythonSysModulesStoredValue::Unrelated;
1889 }
1890 if let Some(path) = python_imported_path(value, source, bindings) {
1891 let root_name = python_static_path(value, source)
1892 .map(|(root, _)| node_text(root, source))
1893 .unwrap_or_default();
1894 let scope = python_sys_modules_value_scope(value, source);
1895 if facts.binding_counts.contains_key(&(
1896 scope.start_byte(),
1897 scope.end_byte(),
1898 root_name.to_owned(),
1899 )) {
1900 return PythonSysModulesStoredValue::Unknown;
1901 }
1902 return if path.join(".") == module {
1903 PythonSysModulesStoredValue::Target
1904 } else {
1905 PythonSysModulesStoredValue::Unrelated
1906 };
1907 }
1908 if value.kind() == "call" {
1909 let Some(function) = value.child_by_field_name("function") else {
1910 return PythonSysModulesStoredValue::Unknown;
1911 };
1912 if python_imported_path(function, source, bindings)
1913 .is_some_and(|path| path == ["types", "ModuleType"])
1914 {
1915 return PythonSysModulesStoredValue::Unrelated;
1916 }
1917 if let Some((root, path)) = python_static_path(function, source)
1918 && path.len() == 1
1919 && python_class_alias_visible_at(node_text(root, source), root, source, &facts.classes)
1920 {
1921 return PythonSysModulesStoredValue::Unrelated;
1922 }
1923 return PythonSysModulesStoredValue::Unknown;
1924 }
1925 let Some((root, path)) = python_static_path(value, source) else {
1926 return PythonSysModulesStoredValue::Unknown;
1927 };
1928 if path.len() == 1 && facts.aliases.contains(node_text(root, source)) {
1929 return PythonSysModulesStoredValue::Target;
1930 }
1931 if path.len() == 1
1932 && python_class_alias_visible_at(node_text(root, source), root, source, &facts.classes)
1933 {
1934 return PythonSysModulesStoredValue::Unrelated;
1935 }
1936 let scope = python_sys_modules_value_scope(value, source);
1937 if scope.kind() == "function_definition"
1938 && scope
1939 .child_by_field_name("parameters")
1940 .is_some_and(|parameters| {
1941 let mut cursor = parameters.walk();
1942 parameters
1943 .named_children(&mut cursor)
1944 .filter_map(|parameter| python_parameter_name(parameter, source))
1945 .any(|parameter| parameter == node_text(root, source))
1946 })
1947 {
1948 return PythonSysModulesStoredValue::Unknown;
1949 }
1950 if !python_sys_modules_has_one_binding(context, scope, node_text(root, source)) {
1951 return PythonSysModulesStoredValue::Unknown;
1952 }
1953 let key = (scope.start_byte(), scope.end_byte(), path.join("."));
1954 if !seen.insert(key.clone()) {
1955 return PythonSysModulesStoredValue::Unknown;
1956 }
1957 let Some([assignment]) = facts.assignments.get(&key).map(Vec::as_slice) else {
1958 return PythonSysModulesStoredValue::Unknown;
1959 };
1960 python_sys_modules_stored_value(*assignment, context, budget, seen)
1961}
1962
1963fn python_sys_modules_update_values<'tree>(
1964 mapping: Node<'tree>,
1965 context: &PythonSysModulesProofContext<'_, 'tree>,
1966 budget: &mut NodeBudget,
1967 seen: &mut HashSet<(usize, usize, String)>,
1968) -> PythonSysModulesStoredValue {
1969 let source = context.source;
1970 let facts = context.facts;
1971 if mapping.kind() == "dictionary" {
1972 let mut cursor = mapping.walk();
1973 for pair in mapping.named_children(&mut cursor) {
1974 let Some(value) = pair.child_by_field_name("value") else {
1975 return PythonSysModulesStoredValue::Unknown;
1976 };
1977 let identity = python_sys_modules_stored_value(value, context, budget, seen);
1978 if identity != PythonSysModulesStoredValue::Unrelated {
1979 return identity;
1980 }
1981 }
1982 return PythonSysModulesStoredValue::Unrelated;
1983 }
1984 let Some((root, path)) = python_static_path(mapping, source) else {
1985 return PythonSysModulesStoredValue::Unknown;
1986 };
1987 let scope = python_sys_modules_value_scope(mapping, source);
1988 let key = (scope.start_byte(), scope.end_byte(), path.join("."));
1989 if !seen.insert(key.clone()) {
1990 return PythonSysModulesStoredValue::Unknown;
1991 }
1992 if !python_sys_modules_has_one_binding(context, scope, node_text(root, source)) {
1993 return PythonSysModulesStoredValue::Unknown;
1994 }
1995 let Some([assigned_value]) = facts.assignments.get(&key).map(Vec::as_slice) else {
1996 return PythonSysModulesStoredValue::Unknown;
1997 };
1998 python_sys_modules_update_values(*assigned_value, context, budget, seen)
1999}
2000
2001fn python_sys_modules_aliases<'tree>(
2005 root: Node<'tree>,
2006 source: &str,
2007 source_module: &str,
2008 module: &str,
2009 bindings: &[crate::imports::PythonImportBinding],
2010 budget: &mut NodeBudget,
2011) -> Option<PythonSysModulesFacts<'tree>> {
2012 let mut aliases_from = HashMap::<String, Vec<String>>::default();
2013 let mut assignments = HashMap::<(usize, usize, String), Vec<Node<'_>>>::default();
2014 let mut binding_counts = HashMap::<(usize, usize, String), usize>::default();
2015 let mut context_managers = HashMap::<(usize, usize, String), Vec<Node<'_>>>::default();
2016 let mut classes = Vec::new();
2017 let mut pending_table_aliases = Vec::<(Vec<String>, Option<Node<'_>>, Node<'_>)>::new();
2018 let mut stack = vec![root];
2019 while let Some(node) = stack.pop() {
2020 if !budget.visit() {
2021 return None;
2022 }
2023 if node.kind() == "class_definition"
2024 && let Some(name) = node.child_by_field_name("name")
2025 {
2026 let scope = python_alias_scope(node);
2027 classes.push(PythonExternalClassAlias {
2028 name: node_text(name, source).to_owned(),
2029 scope_start_byte: scope.start_byte(),
2030 scope_end_byte: scope.end_byte(),
2031 });
2032 }
2033 for binding in python_direct_scope_bindings_bounded(node, source, || budget.visit())? {
2034 if binding.kind != PythonDirectScopeBindingKind::Import {
2035 let scope = python_alias_scope(binding.declaration);
2036 let key = (
2037 scope.start_byte(),
2038 scope.end_byte(),
2039 node_text(binding.declaration, source).to_owned(),
2040 );
2041 *binding_counts.entry(key).or_default() += 1;
2042 }
2043 }
2044 if matches!(node.kind(), "function_definition" | "lambda")
2045 && let Some(parameters) = node.child_by_field_name("parameters")
2046 {
2047 let scope = node;
2048 let mut cursor = parameters.walk();
2049 for parameter in parameters.named_children(&mut cursor) {
2050 if let Some(name) = python_parameter_name(parameter, source) {
2051 *binding_counts
2052 .entry((scope.start_byte(), scope.end_byte(), name))
2053 .or_default() += 1;
2054 }
2055 }
2056 }
2057 if node.kind() == "with_item"
2058 && let Some(pattern) = node
2059 .child_by_field_name("value")
2060 .filter(|value| value.kind() == "as_pattern")
2061 && let Some(manager) = pattern.named_child(0)
2062 && let Some(target) = pattern
2063 .child_by_field_name("alias")
2064 .and_then(|alias| alias.named_child(0))
2065 .filter(|target| target.kind() == "identifier")
2066 {
2067 let scope = python_alias_scope(node);
2068 context_managers
2069 .entry((
2070 scope.start_byte(),
2071 scope.end_byte(),
2072 node_text(target, source).to_owned(),
2073 ))
2074 .or_default()
2075 .push(manager);
2076 }
2077 if matches!(node.kind(), "assignment" | "annotated_assignment") {
2078 let (targets, value) = if node.kind() == "assignment" {
2079 crate::imports::python_assignment_chain(node)
2080 } else {
2081 (
2082 node.child_by_field_name("left").into_iter().collect(),
2083 node.child_by_field_name("right"),
2084 )
2085 };
2086 if let Some(value) = value {
2087 for target in targets {
2088 if let Some((_, path)) = python_static_path(target, source) {
2089 let scope = python_alias_scope(target);
2090 let key = (scope.start_byte(), scope.end_byte(), path.join("."));
2091 assignments.entry(key).or_default().push(value);
2092 }
2093 }
2094 }
2095 }
2096 if node.kind() == "assignment" {
2097 let (targets, value) = crate::imports::python_assignment_chain(node);
2098 if let Some(value) = value
2099 && !targets.is_empty()
2100 && targets.iter().all(|target| target.kind() == "identifier")
2101 {
2102 let target_names = targets
2103 .iter()
2104 .map(|target| node_text(*target, source).to_owned())
2105 .collect::<Vec<_>>();
2106 if python_sys_modules_read_table(value, source).is_some_and(|table| {
2107 python_imported_path(table, source, bindings)
2108 .is_some_and(|path| path == ["sys", "modules"])
2109 }) {
2110 pending_table_aliases.push((
2111 target_names,
2112 if value.kind() == "subscript" {
2113 value.child_by_field_name("subscript")
2114 } else {
2115 python_sys_modules_arguments(value).first().copied()
2116 },
2117 value,
2118 ));
2119 } else if value.kind() == "identifier" {
2120 aliases_from
2121 .entry(node_text(value, source).to_owned())
2122 .or_default()
2123 .extend(target_names);
2124 }
2125 }
2126 }
2127 let mut cursor = node.walk();
2128 stack.extend(node.named_children(&mut cursor));
2129 }
2130
2131 let mut facts = PythonSysModulesFacts {
2132 aliases: HashSet::default(),
2133 assignments,
2134 binding_counts,
2135 context_managers,
2136 classes,
2137 };
2138 let mut target_aliases = Vec::new();
2139 {
2140 let proof = PythonSysModulesProofContext {
2141 source,
2142 source_module,
2143 module,
2144 bindings,
2145 facts: &facts,
2146 };
2147 for (targets, key, read) in pending_table_aliases {
2148 if matches!(
2149 python_sys_modules_read_identity(key, read, &proof, budget),
2150 PythonSysModulesLookup::Target(_)
2151 ) {
2152 target_aliases.extend(targets);
2153 }
2154 }
2155 }
2156 facts.aliases.extend(target_aliases);
2157
2158 let mut pending = facts.aliases.iter().cloned().collect::<Vec<_>>();
2159 while let Some(source_name) = pending.pop() {
2160 if let Some(targets) = aliases_from.get(&source_name) {
2161 for target in targets {
2162 if facts.aliases.insert(target.clone()) {
2163 pending.push(target.clone());
2164 }
2165 }
2166 }
2167 }
2168 Some(facts)
2169}
2170
2171fn python_sys_modules_read_use(
2172 read: Node<'_>,
2173 source: &str,
2174 member: &str,
2175 aliases: &HashSet<String>,
2176) -> Option<PythonModuleMemberMutation> {
2177 let mut expression = read;
2178 let mut direct_attribute = None;
2179 while let Some(parent) = expression.parent() {
2180 if parent.kind() != "attribute"
2181 || parent
2182 .child_by_field_name("object")
2183 .is_none_or(|object| object.id() != expression.id())
2184 {
2185 break;
2186 }
2187 if direct_attribute.is_none() {
2188 direct_attribute = Some(parent);
2189 }
2190 expression = parent;
2191 }
2192 if let Some(attribute) = direct_attribute {
2193 let name = attribute
2194 .child_by_field_name("attribute")
2195 .map(|name| node_text(name, source));
2196 if name == Some("__dict__") {
2197 return Some(PythonModuleMemberMutation::Escape(node_source_range(
2198 attribute,
2199 )));
2200 }
2201 if name == Some(member) && is_binding_target(attribute) {
2202 return Some(PythonModuleMemberMutation::Write(node_source_range(
2203 attribute,
2204 )));
2205 }
2206 return None;
2207 }
2208 if python_builtin_module_read(expression, source) {
2209 return None;
2210 }
2211 if read.parent().is_some_and(|parent| {
2212 parent.kind() == "assignment"
2213 && parent
2214 .child_by_field_name("right")
2215 .is_some_and(|right| right.id() == read.id())
2216 && crate::imports::python_assignment_chain(parent)
2217 .0
2218 .iter()
2219 .all(|target| {
2220 target.kind() == "identifier" && aliases.contains(node_text(*target, source))
2221 })
2222 }) || read.parent().is_some_and(|parent| {
2223 parent.kind() == "expression_statement"
2224 && parent
2225 .named_child(0)
2226 .is_some_and(|expression| expression.id() == read.id())
2227 }) {
2228 return None;
2229 }
2230 Some(PythonModuleMemberMutation::Escape(node_source_range(read)))
2231}
2232
2233fn python_builtin_module_read(expression: Node<'_>, source: &str) -> bool {
2234 let Some(arguments) = expression
2235 .parent()
2236 .filter(|node| node.kind() == "argument_list")
2237 else {
2238 return false;
2239 };
2240 let Some(call) = arguments.parent().filter(|node| node.kind() == "call") else {
2241 return false;
2242 };
2243 let Some(function) = call.child_by_field_name("function") else {
2244 return false;
2245 };
2246 if function.kind() != "identifier"
2247 || !matches!(node_text(function, source), "getattr" | "hasattr")
2248 || matches!(
2249 python_name_resolution_at(node_text(function, source), function, source),
2250 PythonLexicalNameResolution::Local | PythonLexicalNameResolution::Nonlocal
2251 )
2252 {
2253 return false;
2254 }
2255 let mut cursor = arguments.walk();
2256 arguments
2257 .named_children(&mut cursor)
2258 .find(|argument| argument.kind() != "keyword_argument")
2259 .is_some_and(|first| first.id() == expression.id())
2260}
2261
2262fn python_module_table_alias_binding_use(
2263 node: Node<'_>,
2264 source: &str,
2265 aliases: &HashSet<String>,
2266) -> bool {
2267 if node.kind() != "identifier" {
2268 return false;
2269 }
2270 let name = node_text(node, source);
2271 if !aliases.contains(name) {
2272 return false;
2273 }
2274 if is_binding_target(node) {
2275 return true;
2276 }
2277 node.parent().is_some_and(|assignment| {
2278 assignment.kind() == "assignment"
2279 && assignment
2280 .child_by_field_name("right")
2281 .is_some_and(|right| right.id() == node.id())
2282 && crate::imports::python_assignment_chain(assignment)
2283 .0
2284 .iter()
2285 .all(|target| {
2286 target.kind() == "identifier" && aliases.contains(node_text(*target, source))
2287 })
2288 })
2289}
2290
2291pub fn python_external_class_member_mutation(
2298 root: Node<'_>,
2299 source: &str,
2300 class_identities: &[&str],
2301 member: &str,
2302 max_nodes: usize,
2303) -> Option<PythonModuleMemberMutation> {
2304 let bindings = python_import_bindings_from_tree(root, source);
2305 let Some(aliases) =
2306 python_external_class_aliases(root, source, &bindings, class_identities, max_nodes)
2307 else {
2308 return Some(PythonModuleMemberMutation::Budget);
2309 };
2310 let mut budget = NodeBudget::new(max_nodes);
2311 let mut stack = vec![root];
2312 while let Some(node) = stack.pop() {
2313 if !budget.visit() {
2314 return Some(PythonModuleMemberMutation::Budget);
2315 }
2316 if node.kind() == "attribute"
2317 && node
2318 .child_by_field_name("attribute")
2319 .is_some_and(|attribute| node_text(attribute, source) == member)
2320 && is_binding_target(node)
2321 && let Some(object) = node.child_by_field_name("object")
2322 && python_external_class_expression(
2323 object,
2324 source,
2325 &bindings,
2326 class_identities,
2327 &aliases,
2328 )
2329 {
2330 return Some(PythonModuleMemberMutation::Write(node_source_range(node)));
2331 }
2332 if node.kind() == "call"
2333 && python_setattr_may_replace_class_member(
2334 node,
2335 source,
2336 &bindings,
2337 class_identities,
2338 member,
2339 &aliases,
2340 )
2341 {
2342 return Some(PythonModuleMemberMutation::Write(node_source_range(node)));
2343 }
2344 if node.kind() == "call"
2345 && python_mock_patch_may_replace_class_member(
2346 node,
2347 source,
2348 &bindings,
2349 class_identities,
2350 member,
2351 &aliases,
2352 )
2353 {
2354 return Some(PythonModuleMemberMutation::Write(node_source_range(node)));
2355 }
2356 if node.kind() == "identifier"
2357 && python_class_alias_visible_at(node_text(node, source), node, source, &aliases)
2358 && python_class_alias_escapes_at(node, source, &aliases)
2359 {
2360 return Some(PythonModuleMemberMutation::Escape(node_source_range(node)));
2361 }
2362 let mut cursor = node.walk();
2363 stack.extend(node.named_children(&mut cursor));
2364 }
2365 None
2366}
2367
2368#[derive(Clone)]
2369struct PythonExternalClassAlias {
2370 name: String,
2371 scope_start_byte: usize,
2372 scope_end_byte: usize,
2373}
2374
2375fn python_external_class_aliases(
2376 root: Node<'_>,
2377 source: &str,
2378 bindings: &[crate::imports::PythonImportBinding],
2379 class_identities: &[&str],
2380 max_nodes: usize,
2381) -> Option<Vec<PythonExternalClassAlias>> {
2382 let mut budget = NodeBudget::new(max_nodes);
2383 let mut assignments = Vec::new();
2384 let mut stack = vec![root];
2385 while let Some(node) = stack.pop() {
2386 if !budget.visit() {
2387 return None;
2388 }
2389 if matches!(node.kind(), "assignment" | "annotated_assignment")
2390 && let Some(target) = node.child_by_field_name("left")
2391 && target.kind() == "identifier"
2392 && let Some(value) = node.child_by_field_name("right")
2393 {
2394 let scope = python_alias_scope(node);
2395 assignments.push((
2396 node_text(target, source).to_owned(),
2397 value,
2398 scope.start_byte(),
2399 scope.end_byte(),
2400 ));
2401 }
2402 let mut cursor = node.walk();
2403 stack.extend(node.named_children(&mut cursor));
2404 }
2405
2406 let mut aliases = Vec::<PythonExternalClassAlias>::new();
2407 loop {
2408 let before = aliases.len();
2409 for (name, value, scope_start_byte, scope_end_byte) in &assignments {
2410 if python_external_class_expression(
2411 *value,
2412 source,
2413 bindings,
2414 class_identities,
2415 &aliases,
2416 ) && !aliases.iter().any(|alias| {
2417 alias.name == *name
2418 && alias.scope_start_byte == *scope_start_byte
2419 && alias.scope_end_byte == *scope_end_byte
2420 }) {
2421 aliases.push(PythonExternalClassAlias {
2422 name: name.clone(),
2423 scope_start_byte: *scope_start_byte,
2424 scope_end_byte: *scope_end_byte,
2425 });
2426 }
2427 }
2428 if aliases.len() == before {
2429 return Some(aliases);
2430 }
2431 }
2432}
2433
2434fn python_alias_scope(mut node: Node<'_>) -> Node<'_> {
2435 while let Some(parent) = node.parent() {
2436 if matches!(
2437 parent.kind(),
2438 "module" | "class_definition" | "function_definition" | "lambda"
2439 ) {
2440 return parent;
2441 }
2442 node = parent;
2443 }
2444 node
2445}
2446
2447fn python_external_class_expression(
2448 expression: Node<'_>,
2449 source: &str,
2450 bindings: &[crate::imports::PythonImportBinding],
2451 class_identities: &[&str],
2452 aliases: &[PythonExternalClassAlias],
2453) -> bool {
2454 if python_imported_class_expression(expression, source, bindings, class_identities) {
2455 return true;
2456 }
2457 let Some((root, path)) = python_static_path(expression, source) else {
2458 return false;
2459 };
2460 path.len() == 1 && python_class_alias_visible_at(node_text(root, source), root, source, aliases)
2461}
2462
2463fn python_class_alias_visible_at(
2464 name: &str,
2465 reference: Node<'_>,
2466 source: &str,
2467 aliases: &[PythonExternalClassAlias],
2468) -> bool {
2469 if !aliases.iter().any(|alias| alias.name == name) {
2470 return false;
2471 }
2472 let mut current = reference;
2473 while let Some(scope) = nearest_scope(current) {
2474 if aliases.iter().any(|alias| {
2475 alias.name == name
2476 && alias.scope_start_byte == scope.start_byte()
2477 && alias.scope_end_byte == scope.end_byte()
2478 }) {
2479 return true;
2480 }
2481 match scope.kind() {
2482 "function_definition" | "lambda" => {
2483 let parameters = scope
2484 .child_by_field_name("parameters")
2485 .into_iter()
2486 .flat_map(|parameters| {
2487 let mut cursor = parameters.walk();
2488 parameters
2489 .named_children(&mut cursor)
2490 .filter_map(|parameter| python_parameter_name(parameter, source))
2491 .collect::<Vec<_>>()
2492 });
2493 let Some(inventory) =
2494 PythonLexicalScopeInventory::collect_bounded(scope, source, parameters, || {
2495 true
2496 })
2497 else {
2498 return true;
2499 };
2500 match inventory.name_resolution_at(name, reference) {
2501 PythonLexicalNameResolution::Local => return false,
2502 PythonLexicalNameResolution::Global => break,
2503 PythonLexicalNameResolution::Nonlocal
2504 | PythonLexicalNameResolution::Unbound => {}
2505 }
2506 }
2507 "module" => return false,
2508 "class_definition" => {}
2511 _ => unreachable!("nearest_scope returns only Python lexical scopes"),
2512 }
2513 current = scope;
2514 }
2515 aliases.iter().any(|alias| {
2516 alias.name == name
2517 && alias.scope_start_byte == 0
2518 && reference.start_byte() < alias.scope_end_byte
2519 })
2520}
2521
2522fn nearest_scope(mut node: Node<'_>) -> Option<Node<'_>> {
2523 while let Some(parent) = node.parent() {
2524 if matches!(
2525 parent.kind(),
2526 "module" | "class_definition" | "function_definition" | "lambda"
2527 ) {
2528 return Some(parent);
2529 }
2530 node = parent;
2531 }
2532 None
2533}
2534
2535fn python_class_alias_escapes_at(
2536 node: Node<'_>,
2537 source: &str,
2538 aliases: &[PythonExternalClassAlias],
2539) -> bool {
2540 let mut current = node;
2541 while let Some(parent) = current.parent() {
2542 if matches!(
2543 parent.kind(),
2544 "function_definition" | "lambda" | "class_definition" | "module"
2545 ) {
2546 return false;
2547 }
2548 if parent.kind() == "argument_list"
2549 && parent.parent().is_some_and(|call| call.kind() == "call")
2550 {
2551 return true;
2552 }
2553 if matches!(parent.kind(), "return_statement" | "yield") {
2554 return true;
2555 }
2556 if matches!(parent.kind(), "list" | "tuple" | "set" | "dictionary") {
2557 return true;
2558 }
2559 if matches!(parent.kind(), "assignment" | "annotated_assignment")
2560 && parent.child_by_field_name("right").is_some_and(|right| {
2561 right.start_byte() <= node.start_byte() && node.end_byte() <= right.end_byte()
2562 })
2563 {
2564 let Some(target) = parent.child_by_field_name("left") else {
2565 return true;
2566 };
2567 if target.kind() == "identifier"
2568 && python_class_alias_visible_at(node_text(target, source), target, source, aliases)
2569 {
2570 return false;
2571 }
2572 return true;
2573 }
2574 current = parent;
2575 }
2576 false
2577}
2578
2579fn python_setattr_may_replace_class_member(
2580 call: Node<'_>,
2581 source: &str,
2582 bindings: &[crate::imports::PythonImportBinding],
2583 class_identities: &[&str],
2584 member: &str,
2585 aliases: &[PythonExternalClassAlias],
2586) -> bool {
2587 let Some(function) = call.child_by_field_name("function") else {
2588 return false;
2589 };
2590 let imported_setattr = python_imported_path(function, source, bindings)
2591 .is_some_and(|path| path == ["builtins", "setattr"]);
2592 let builtin_setattr = python_static_path(function, source).is_some_and(|(root, path)| {
2593 path == ["setattr"]
2594 && !matches!(
2595 python_name_resolution_at(node_text(root, source), root, source),
2596 PythonLexicalNameResolution::Local | PythonLexicalNameResolution::Nonlocal
2597 )
2598 });
2599 if !imported_setattr && !builtin_setattr {
2600 return false;
2601 }
2602 let Some(arguments) = call.child_by_field_name("arguments") else {
2603 return false;
2604 };
2605 let mut cursor = arguments.walk();
2606 let positional = arguments
2607 .named_children(&mut cursor)
2608 .filter(|argument| argument.kind() != "keyword_argument")
2609 .collect::<Vec<_>>();
2610 let [object, name, ..] = positional.as_slice() else {
2611 return false;
2612 };
2613 if !python_external_class_expression(*object, source, bindings, class_identities, aliases) {
2614 return false;
2615 }
2616 python_plain_string_literal(*name, source).is_none_or(|name| name == member)
2617}
2618
2619fn python_mock_patch_may_replace_class_member(
2620 call: Node<'_>,
2621 source: &str,
2622 bindings: &[crate::imports::PythonImportBinding],
2623 class_identities: &[&str],
2624 member: &str,
2625 aliases: &[PythonExternalClassAlias],
2626) -> bool {
2627 let Some(function) = call.child_by_field_name("function") else {
2628 return false;
2629 };
2630 let Some(function_path) = python_imported_path(function, source, bindings) else {
2631 return false;
2632 };
2633 let patch_object = function_path == ["unittest", "mock", "patch", "object"];
2634 let patch_class_path = function_path == ["unittest", "mock", "patch"];
2635 if !patch_object && !patch_class_path {
2636 return false;
2637 }
2638 let Some(arguments) = call.child_by_field_name("arguments") else {
2639 return false;
2640 };
2641 let mut cursor = arguments.walk();
2642 let positional = arguments
2643 .named_children(&mut cursor)
2644 .filter(|argument| argument.kind() != "keyword_argument")
2645 .collect::<Vec<_>>();
2646 let Some(target) = positional.first().copied() else {
2647 return false;
2648 };
2649 if patch_object {
2650 let Some(name) = positional.get(1).copied() else {
2651 return false;
2652 };
2653 return python_external_class_expression(
2654 target,
2655 source,
2656 bindings,
2657 class_identities,
2658 aliases,
2659 ) && python_plain_string_literal(name, source).is_none_or(|name| name == member);
2660 }
2661 let Some(target_path) = python_plain_string_literal(target, source) else {
2662 return false;
2663 };
2664 class_identities
2665 .iter()
2666 .any(|class| target_path == *class || target_path == format!("{class}.{member}"))
2667}
2668
2669fn python_imported_path(
2670 expression: Node<'_>,
2671 source: &str,
2672 bindings: &[crate::imports::PythonImportBinding],
2673) -> Option<Vec<String>> {
2674 let (root, expression_path) = python_static_path(expression, source)?;
2675 let root_name = node_text(root, source);
2676 if matches!(
2677 python_name_resolution_at(root_name, root, source),
2678 PythonLexicalNameResolution::Local | PythonLexicalNameResolution::Nonlocal
2679 ) {
2680 return None;
2681 }
2682 let latest_import = bindings
2683 .iter()
2684 .filter(|binding| {
2685 binding.local_name == root_name
2686 && binding.start_byte <= root.start_byte()
2687 && binding.scope_start_byte <= root.start_byte()
2688 && root.end_byte() <= binding.scope_end_byte
2689 })
2690 .map(|binding| binding.start_byte)
2691 .max()?;
2692 let binding = bindings.iter().find(|binding| {
2693 binding.local_name == root_name
2694 && binding.start_byte == latest_import
2695 && binding.scope_start_byte <= root.start_byte()
2696 && root.end_byte() <= binding.scope_end_byte
2697 })?;
2698 let qualified_prefix = if binding.consumed_attributes > 0 {
2699 binding.qualified_name.split('.').next()?
2700 } else {
2701 binding.qualified_name.as_str()
2702 };
2703 let mut qualified_path = qualified_prefix
2704 .split('.')
2705 .map(str::to_owned)
2706 .collect::<Vec<_>>();
2707 qualified_path.extend(expression_path.into_iter().skip(1));
2708 Some(qualified_path)
2709}
2710
2711fn python_imported_class_expression(
2712 expression: Node<'_>,
2713 source: &str,
2714 bindings: &[crate::imports::PythonImportBinding],
2715 class_identities: &[&str],
2716) -> bool {
2717 let Some(path) = python_imported_path(expression, source, bindings) else {
2718 return false;
2719 };
2720 class_identities.iter().any(|identity| {
2721 identity
2722 .split('.')
2723 .map(str::to_owned)
2724 .eq(path.iter().cloned())
2725 })
2726}
2727
2728fn python_static_path<'tree>(
2729 node: Node<'tree>,
2730 source: &str,
2731) -> Option<(Node<'tree>, Vec<String>)> {
2732 let mut current = node;
2733 let mut attributes = Vec::new();
2734 loop {
2735 if current.kind() == "identifier" {
2736 attributes.reverse();
2737 let mut path = Vec::with_capacity(attributes.len() + 1);
2738 path.push(node_text(current, source).to_owned());
2739 path.extend(attributes);
2740 return Some((current, path));
2741 }
2742 if current.kind() != "attribute" {
2743 return None;
2744 }
2745 let attribute = current.child_by_field_name("attribute")?;
2746 attributes.push(node_text(attribute, source).to_owned());
2747 current = current.child_by_field_name("object")?;
2748 }
2749}
2750
2751fn module_binding_use(
2753 node: Node<'_>,
2754 source: &str,
2755 bindings: &[crate::imports::PythonImportBinding],
2756 table_aliases: &HashSet<String>,
2757 module: &str,
2758 member: &str,
2759) -> Option<PythonModuleMemberMutation> {
2760 let Some(parent) = node.parent() else {
2761 return Some(PythonModuleMemberMutation::Escape(node_source_range(node)));
2762 };
2763 if matches!(
2764 parent.kind(),
2765 "import_statement" | "import_from_statement" | "aliased_import" | "dotted_name"
2766 ) {
2767 return None;
2768 }
2769 let mut expression = node;
2770 while let Some(parent) = expression.parent() {
2771 if parent.kind() != "attribute"
2772 || parent
2773 .child_by_field_name("object")
2774 .is_none_or(|object| object.id() != expression.id())
2775 {
2776 break;
2777 }
2778 expression = parent;
2779 }
2780 if python_builtin_module_read(expression, source) {
2781 return None;
2782 }
2783 let imported_path = python_imported_path(expression, source, bindings);
2784 let table_alias_path = python_static_path(expression, source).and_then(|(root, suffix)| {
2785 let root_name = node_text(root, source);
2786 table_aliases.contains(root_name).then(|| {
2787 let mut resolved = vec![module.to_owned()];
2788 resolved.extend(suffix.into_iter().skip(1));
2789 resolved
2790 })
2791 });
2792 let Some(path) = imported_path.or(table_alias_path) else {
2793 return Some(PythonModuleMemberMutation::Escape(node_source_range(node)));
2794 };
2795 let path = path.join(".");
2796 let target = format!("{module}.{member}");
2797 if path == target {
2798 return is_binding_target(expression)
2799 .then(|| PythonModuleMemberMutation::Write(node_source_range(expression)));
2800 }
2801 if path == format!("{module}.__dict__") || path == module {
2802 return Some(PythonModuleMemberMutation::Escape(node_source_range(
2803 expression,
2804 )));
2805 }
2806 if path.starts_with(&format!("{module}.")) {
2807 None
2808 } else {
2809 Some(PythonModuleMemberMutation::Escape(node_source_range(node)))
2810 }
2811}
2812
2813#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2815enum PythonTestHelperCall<'tree> {
2816 MockPatch,
2818 MockPatchDict,
2820 MonkeyPatchSetItem(Node<'tree>),
2823 MonkeyPatchDelItem(Node<'tree>),
2825}
2826
2827fn python_test_helper_call<'tree>(
2828 call: Node<'tree>,
2829 source: &str,
2830 bindings: &[crate::imports::PythonImportBinding],
2831) -> Option<PythonTestHelperCall<'tree>> {
2832 let function = call.child_by_field_name("function")?;
2833 if let Some(path) = python_imported_path(function, source, bindings)
2834 && let Some(patch) = path.windows(2).rposition(|pair| pair == ["mock", "patch"])
2835 {
2836 return match &path[patch + 2..] {
2837 [] => Some(PythonTestHelperCall::MockPatch),
2838 [method] if method == "dict" => Some(PythonTestHelperCall::MockPatchDict),
2839 _ => None,
2840 };
2841 }
2842 if function.kind() != "attribute" {
2843 return None;
2844 }
2845 let receiver = function.child_by_field_name("object")?;
2846 match node_text(function.child_by_field_name("attribute")?, source) {
2847 "setitem" => Some(PythonTestHelperCall::MonkeyPatchSetItem(receiver)),
2848 "delitem" => Some(PythonTestHelperCall::MonkeyPatchDelItem(receiver)),
2849 _ => None,
2850 }
2851}
2852
2853fn python_mock_patch_may_replace_module_member(
2854 call: Node<'_>,
2855 source: &str,
2856 bindings: &[crate::imports::PythonImportBinding],
2857 module: &str,
2858 member: &str,
2859) -> bool {
2860 if python_test_helper_call(call, source, bindings) != Some(PythonTestHelperCall::MockPatch) {
2861 return false;
2862 }
2863 let Some(arguments) = call.child_by_field_name("arguments") else {
2864 return false;
2865 };
2866 let mut cursor = arguments.walk();
2867 let Some(target) = arguments.named_children(&mut cursor).next() else {
2868 return false;
2869 };
2870 let expected = format!("{module}.{member}");
2871 python_plain_string_literal(target, source) == Some(expected.as_str())
2872}
2873
2874#[cfg(test)]
2875mod tests {
2876 use super::*;
2877
2878 fn facts(source: &str) -> PythonRuntimeReadFacts {
2879 let mut parser = tree_sitter::Parser::new();
2880 parser
2881 .set_language(&tree_sitter_python::LANGUAGE.into())
2882 .expect("python grammar loads");
2883 let tree = parser.parse(source, None).expect("fixture parses");
2884 extract_python_runtime_reads(tree.root_node(), source, 64)
2885 }
2886
2887 #[test]
2888 #[brokk_bifrost_test_macros::hardwon(
2889 "6b123df7cc68: quicksilver round 6 on brokk-bifrost-python: module alias exhaustion certified mutation-free"
2890 )]
2891 fn qs6_module_mutation_budget_is_not_absence() {
2892 let source = "import os\nos.system = fake\n";
2893 let tree = crate::declarations::parse_python_tree(source).unwrap();
2894 for budget in [0, 1, 2] {
2895 assert_eq!(
2896 python_module_member_mutation(
2897 tree.root_node(),
2898 source,
2899 "test",
2900 "os",
2901 "system",
2902 budget
2903 ),
2904 Some(PythonModuleMemberMutation::Budget),
2905 "budget={budget}"
2906 );
2907 }
2908 assert!(
2909 python_module_member_mutation(tree.root_node(), source, "test", "os", "system", 4096)
2910 .is_some()
2911 );
2912 }
2913
2914 #[test]
2917 #[brokk_bifrost_test_macros::hardwon(
2918 "6b123df7cc68: quicksilver round 6 on brokk-bifrost-python: runtime indices lost radix and separator literals"
2919 )]
2920 fn qs6_runtime_key_shapes() {
2921 use PythonRuntimeAccessKey::{Dynamic, Index, Property, Unsupported};
2922 for (key, expected) in [
2923 ("0", Index(0)),
2924 ("0x1", Index(1)),
2925 ("0b1", Index(1)),
2926 ("0o7", Index(7)),
2927 ("1_0", Index(10)),
2928 ("340282366920938463463374607431768211455", Index(u128::MAX)),
2929 ("340282366920938463463374607431768211456", Unsupported),
2930 ("1:3", Unsupported),
2931 ("1, 2", Unsupported),
2932 ("1,", Unsupported),
2933 ("(1, 2)", Unsupported),
2934 ("name", Dynamic),
2935 ("\"HOME\"", Property("HOME".into())),
2936 ] {
2937 let source = format!("import sys\nvalue = sys.argv[{key}]\n");
2938 let facts = facts(&source);
2939 assert!(facts.complete, "{facts:?}");
2940 assert_eq!(facts.reads.len(), 1, "{facts:?}");
2941 assert_eq!(facts.reads[0].access, expected, "{key}");
2942 }
2943 }
2944
2945 #[test]
2946 fn qs6_runtime_extraction_bounds() {
2947 let source = "pass\n".repeat(100);
2948 let tree = crate::declarations::parse_python_tree(&source).unwrap();
2949 let empty = extract_python_runtime_reads(tree.root_node(), &source, 1);
2950 assert!(empty.complete, "{empty:?}");
2951 assert!(empty.visited_nodes > 64, "{empty:?}");
2952 let source = "import sys\nx = sys.argv[1]\ny = sys.argv[2]\n";
2953 let tree = crate::declarations::parse_python_tree(source).unwrap();
2954 for cap in [0, 1, 2, 3] {
2955 let facts = extract_python_runtime_reads(tree.root_node(), source, cap);
2956 assert_eq!(facts.reads.len(), cap.min(2), "{facts:?}");
2957 assert_eq!(facts.complete, cap > 2, "{facts:?}");
2958 }
2959 }
2960
2961 #[test]
2963 fn imported_module_environment_subscript_is_a_pristine_keyed_read() {
2964 let source = "import os\n\n\ndef run():\n command = os.environ[\"DFB\"]\n os.system(command)\n";
2965 let facts = facts(source);
2966 assert!(facts.complete, "{facts:#?}");
2967 let [read] = facts.reads.as_slice() else {
2968 panic!("exactly one keyed read: {facts:#?}");
2969 };
2970 assert_eq!(read.root_name, "os");
2971 assert_eq!(read.container, "environ");
2972 assert_eq!(
2973 read.access,
2974 PythonRuntimeAccessKey::Property("DFB".to_owned())
2975 );
2976 assert_eq!(
2977 read.lexical_resolution,
2978 PythonRuntimeRootResolution::ImportedModule
2979 );
2980 assert_eq!(read.mutation, PythonRuntimeMutationEvidence::NoKnownWrite);
2981 assert_eq!(
2982 read.accessor,
2983 PythonRuntimeAccessorCoverage::NoKnownAccessorEffects,
2984 "the only call runs after the read"
2985 );
2986 assert!(facts.writes.is_empty(), "{facts:#?}");
2987 assert_eq!(
2990 &source[read.candidate_anchor.start_byte..read.candidate_anchor.end_byte],
2991 "os.environ"
2992 );
2993 assert_eq!(
2994 &source[read.range.start_byte..read.range.end_byte],
2995 "os.environ[\"DFB\"]"
2996 );
2997 }
2998
2999 #[test]
3000 fn imported_module_argv_index_is_a_keyed_read() {
3001 let facts = facts("import sys\n\n\ndef run():\n return sys.argv[1]\n");
3002 let [read] = facts.reads.as_slice() else {
3003 panic!("exactly one keyed read: {facts:#?}");
3004 };
3005 assert_eq!(read.root_name, "sys");
3006 assert_eq!(read.container, "argv");
3007 assert_eq!(read.access, PythonRuntimeAccessKey::Index(1));
3008 }
3009
3010 #[test]
3013 fn a_shadowed_module_name_is_not_a_read() {
3014 let facts = facts("import os\n\n\ndef run(os):\n return os.environ[\"DFB\"]\n");
3015 assert!(facts.reads.is_empty(), "{facts:#?}");
3016 }
3017
3018 #[test]
3019 fn a_local_container_name_is_not_a_read() {
3020 let facts =
3021 facts("def run():\n environ = {\"DFB\": \"x\"}\n return environ[\"DFB\"]\n");
3022 assert!(facts.reads.is_empty(), "{facts:#?}");
3023 assert!(facts.writes.is_empty(), "{facts:#?}");
3024 }
3025
3026 #[test]
3027 fn an_unrelated_module_spelled_os_is_not_a_read() {
3028 let facts = facts("from mypkg import os\n\n\ndef run():\n return os.environ[\"DFB\"]\n");
3029 assert!(facts.reads.is_empty(), "{facts:#?}");
3030 }
3031
3032 #[test]
3033 fn a_dynamic_key_stays_a_read_with_no_static_key() {
3034 let facts = facts("import os\n\n\ndef run(name):\n return os.environ[name]\n");
3035 let [read] = facts.reads.as_slice() else {
3036 panic!("one read: {facts:#?}");
3037 };
3038 assert_eq!(read.access, PythonRuntimeAccessKey::Dynamic);
3039 }
3040
3041 #[test]
3042 fn a_container_write_refutes_every_read_of_that_container() {
3043 let facts = facts(
3044 "import os\n\n\ndef run():\n os.environ[\"A\"] = \"1\"\n return os.environ[\"B\"]\n",
3045 );
3046 let [read] = facts.reads.as_slice() else {
3047 panic!("one read: {facts:#?}");
3048 };
3049 assert_eq!(read.mutation, PythonRuntimeMutationEvidence::KnownWrite);
3050 let [write] = facts.writes.as_slice() else {
3051 panic!("one write: {facts:#?}");
3052 };
3053 assert_eq!(write.target, PythonRuntimeWriteTarget::Key);
3054 assert_eq!(write.container.as_deref(), Some("environ"));
3055 }
3056
3057 #[test]
3058 fn a_container_method_receiver_refutes_the_pristine_claim() {
3059 let facts = facts(
3060 "import os\n\n\ndef run():\n os.environ.update({})\n return os.environ[\"B\"]\n",
3061 );
3062 let [read] = facts.reads.as_slice() else {
3063 panic!("one read: {facts:#?}");
3064 };
3065 assert_eq!(
3066 read.mutation,
3067 PythonRuntimeMutationEvidence::KnownWriteAndUnknownEffects
3068 );
3069 }
3070
3071 #[test]
3072 fn a_module_object_escape_refutes_every_container() {
3073 let facts =
3074 facts("import os\n\n\ndef run(sink):\n sink(os)\n return os.environ[\"B\"]\n");
3075 let [read] = facts.reads.as_slice() else {
3076 panic!("one read: {facts:#?}");
3077 };
3078 assert_eq!(
3079 read.mutation,
3080 PythonRuntimeMutationEvidence::KnownWriteAndUnknownEffects
3081 );
3082 assert!(
3083 facts
3084 .writes
3085 .iter()
3086 .any(|write| write.target == PythonRuntimeWriteTarget::ModuleEscape),
3087 "{facts:#?}"
3088 );
3089 }
3090
3091 #[test]
3092 fn a_call_before_the_read_leaves_accessor_coverage_open() {
3093 let facts = facts(
3094 "import os\n\n\ndef run(prepare):\n prepare()\n return os.environ[\"B\"]\n",
3095 );
3096 let [read] = facts.reads.as_slice() else {
3097 panic!("one read: {facts:#?}");
3098 };
3099 assert_eq!(
3100 read.accessor,
3101 PythonRuntimeAccessorCoverage::UnknownAccessorEffects
3102 );
3103 }
3104
3105 #[test]
3108 fn a_call_in_another_function_does_not_poison_the_read() {
3109 let source = "import os\n\n\ndef other(prepare):\n prepare()\n\n\ndef run():\n return os.environ[\"B\"]\n";
3110 let facts = facts(source);
3111 let [read] = facts.reads.as_slice() else {
3112 panic!("one read: {facts:#?}");
3113 };
3114 assert_eq!(
3115 read.accessor,
3116 PythonRuntimeAccessorCoverage::NoKnownAccessorEffects
3117 );
3118 }
3119
3120 #[test]
3121 fn a_call_later_in_the_same_loop_can_precede_the_read() {
3122 let source = "import os\n\n\ndef run(items, prepare):\n for item in items:\n value = os.environ[\"B\"]\n prepare(value)\n";
3123 let facts = facts(source);
3124 let [read] = facts.reads.as_slice() else {
3125 panic!("one read: {facts:#?}");
3126 };
3127 assert_eq!(
3128 read.accessor,
3129 PythonRuntimeAccessorCoverage::UnknownAccessorEffects
3130 );
3131 }
3132
3133 #[test]
3134 fn an_aliased_import_keeps_the_absolute_module_identity() {
3135 let facts =
3136 facts("import os as operating\n\n\ndef run():\n return operating.environ[\"B\"]\n");
3137 let [read] = facts.reads.as_slice() else {
3138 panic!("one read: {facts:#?}");
3139 };
3140 assert_eq!(read.binding_name, "operating");
3141 assert_eq!(read.root_name, "os");
3142 }
3143
3144 #[test]
3147 fn a_submodule_import_agrees_with_its_package_binding() {
3148 let facts =
3149 facts("import os\nimport os.path\n\n\ndef run():\n return os.environ[\"B\"]\n");
3150 let [read] = facts.reads.as_slice() else {
3151 panic!("one read: {facts:#?}");
3152 };
3153 assert_eq!(read.root_name, "os");
3154 }
3155
3156 #[test]
3157 fn a_module_scope_rebinding_of_the_import_name_is_not_a_read() {
3158 let facts = facts("import os\n\nos = None\n\n\ndef run():\n return os.environ[\"B\"]\n");
3159 assert!(facts.reads.is_empty(), "{facts:#?}");
3160 }
3161}
3162
3163#[cfg(test)]
3164mod module_member_tests {
3165 use super::*;
3166
3167 fn mutation(source: &str, member: &str) -> Option<PythonModuleMemberMutation> {
3168 let mut parser = tree_sitter::Parser::new();
3169 parser
3170 .set_language(&tree_sitter_python::LANGUAGE.into())
3171 .expect("python grammar loads");
3172 let tree = parser.parse(source, None).expect("fixture parses");
3173 python_module_member_mutation(tree.root_node(), source, "test", "os", member, 4096)
3174 }
3175
3176 #[test]
3177 fn a_member_assignment_refuses_that_member() {
3178 let found = mutation(
3179 "import os\n\n\ndef patch(fake):\n os.system = fake\n",
3180 "system",
3181 );
3182 assert!(
3183 matches!(found, Some(PythonModuleMemberMutation::Write(_))),
3184 "{found:?}"
3185 );
3186 assert_eq!(
3188 mutation(
3189 "import os\n\n\ndef patch(fake):\n os.getcwd = fake\n",
3190 "system"
3191 ),
3192 None
3193 );
3194 }
3195
3196 #[test]
3197 fn a_module_object_escape_refuses_every_member() {
3198 for source in [
3199 "import os\n\n\ndef leak(sink):\n sink(os)\n",
3200 "import os\n\n\nalias = os\n",
3201 "import os\nimport functools\n\n\ndef patch():\n setattr(os, \"system\", None)\n",
3202 ] {
3203 let found = mutation(source, "system");
3204 assert!(
3205 matches!(found, Some(PythonModuleMemberMutation::Escape(_))),
3206 "{source}: {found:?}"
3207 );
3208 }
3209 }
3210
3211 #[test]
3212 fn a_matching_module_table_read_counts_its_attribute_write() {
3213 let found = mutation(
3214 "import sys\n\n\ndef patch(fake):\n sys.modules[\"os\"].system = fake\n",
3215 "system",
3216 );
3217 assert!(
3218 matches!(found, Some(PythonModuleMemberMutation::Write(_))),
3219 "{found:?}"
3220 );
3221 }
3222
3223 #[test]
3224 fn a_file_that_never_binds_the_module_proves_the_absence() {
3225 assert_eq!(mutation("def run():\n return 1\n", "system"), None);
3226 }
3227}
3228
3229#[cfg(test)]
3230mod shadowing_tests {
3231 use super::*;
3232
3233 fn facts(source: &str) -> PythonRuntimeReadFacts {
3234 let mut parser = tree_sitter::Parser::new();
3235 parser
3236 .set_language(&tree_sitter_python::LANGUAGE.into())
3237 .expect("python grammar loads");
3238 let tree = parser.parse(source, None).expect("fixture parses");
3239 extract_python_runtime_reads(tree.root_node(), source, 64)
3240 }
3241
3242 #[test]
3245 fn a_parameter_spelling_the_module_name_is_not_an_escape() {
3246 let source = "import os\n\n\ndef read_shadowed(os):\n return os.environ[\"A\"]\n\n\ndef read_real():\n return os.environ[\"B\"]\n";
3247 let facts = facts(source);
3248 assert!(facts.writes.is_empty(), "{facts:#?}");
3249 let [read] = facts.reads.as_slice() else {
3250 panic!("only the unshadowed read is a candidate: {facts:#?}");
3251 };
3252 assert_eq!(read.mutation, PythonRuntimeMutationEvidence::NoKnownWrite);
3253 assert_eq!(
3254 &source[read.key_range.start_byte..read.key_range.end_byte],
3255 "\"B\""
3256 );
3257 }
3258
3259 #[test]
3260 fn a_parameter_spelling_the_module_name_does_not_refuse_a_member() {
3261 let source =
3262 "import os\n\n\ndef helper(os):\n return os\n\n\ndef run(c):\n os.system(c)\n";
3263 let mut parser = tree_sitter::Parser::new();
3264 parser
3265 .set_language(&tree_sitter_python::LANGUAGE.into())
3266 .expect("python grammar loads");
3267 let tree = parser.parse(source, None).expect("fixture parses");
3268 assert_eq!(
3269 python_module_member_mutation(tree.root_node(), source, "test", "os", "system", 4096),
3270 None
3271 );
3272 }
3273}
3274
3275#[cfg(test)]
3276mod external_class_mutation_tests {
3277 use super::*;
3278 use brokk_bifrost_test_macros::hardwon;
3279
3280 fn mutation(source: &str) -> Option<PythonModuleMemberMutation> {
3281 let mut parser = tree_sitter::Parser::new();
3282 parser
3283 .set_language(&tree_sitter_python::LANGUAGE.into())
3284 .expect("python grammar loads");
3285 let tree = parser.parse(source, None).expect("fixture parses");
3286 python_external_class_member_mutation(
3287 tree.root_node(),
3288 source,
3289 &["paramiko.client.SSHClient"],
3290 "set_missing_host_key_policy",
3291 4096,
3292 )
3293 }
3294
3295 #[test]
3296 fn exact_imported_class_member_writes_are_found() {
3297 for source in [
3298 "from paramiko.client import SSHClient as Client\n\nClient.set_missing_host_key_policy = fake\n",
3299 "import paramiko.client as client\n\nclient.SSHClient.set_missing_host_key_policy = fake\n",
3300 "from paramiko.client import SSHClient as Client\n\nsetattr(Client, 'set_missing_host_key_policy', fake)\n",
3301 "from paramiko.client import SSHClient as Client\n\nsetattr(Client, name, fake)\n",
3302 ] {
3303 assert!(
3304 matches!(mutation(source), Some(PythonModuleMemberMutation::Write(_))),
3305 "{source}"
3306 );
3307 }
3308 }
3309
3310 #[test]
3311 #[hardwon("#4411: class aliases and mock patch APIs left external method dispatch exact")]
3312 fn class_aliases_and_mock_patch_apis_refute_method_closure() {
3313 for source in [
3314 "from paramiko.client import SSHClient as C\nAlias = C\nAlias.set_missing_host_key_policy = fake\n",
3315 "from paramiko.client import SSHClient as C\nAlias = C\nsetattr(Alias, 'set_missing_host_key_policy', fake)\n",
3316 "from unittest import mock\nfrom paramiko.client import SSHClient as C\nmock.patch.object(C, 'set_missing_host_key_policy', fake)\n",
3317 "from unittest.mock import patch\nfrom paramiko.client import SSHClient as C\npatch.object(C, 'set_missing_host_key_policy', fake)\n",
3318 "from unittest import mock\nmock.patch('paramiko.client.SSHClient.set_missing_host_key_policy', fake)\n",
3319 ] {
3320 assert!(
3321 matches!(mutation(source), Some(PythonModuleMemberMutation::Write(_))),
3322 "{source}"
3323 );
3324 }
3325 }
3326
3327 #[test]
3328 #[hardwon("#4411: an escaped class alias left external method dispatch exact")]
3329 fn an_escaped_class_alias_refutes_method_closure() {
3330 assert!(matches!(
3331 mutation("from paramiko.client import SSHClient as C\nAlias = C\nconsume(Alias)\n"),
3332 Some(PythonModuleMemberMutation::Escape(_))
3333 ));
3334 }
3335
3336 #[test]
3337 fn constructor_uses_and_same_named_workspace_classes_are_not_writes() {
3338 assert_eq!(
3339 mutation(
3340 "from paramiko.client import SSHClient as Client\n\n\ndef connect():\n client = Client()\n client.set_missing_host_key_policy(policy)\n"
3341 ),
3342 None
3343 );
3344 assert_eq!(
3345 mutation(
3346 "class SSHClient:\n pass\n\nSSHClient.set_missing_host_key_policy = fake\n"
3347 ),
3348 None
3349 );
3350 }
3351}
3352
3353#[cfg(test)]
3354mod module_member_mutation_tests {
3355 use super::*;
3356 use brokk_bifrost_test_macros::hardwon;
3357
3358 fn mutation(source: &str) -> Option<PythonModuleMemberMutation> {
3359 mutation_in(source, "pkg.M")
3360 }
3361
3362 fn mutation_in(source: &str, source_module: &str) -> Option<PythonModuleMemberMutation> {
3363 mutation_for_module(source, source_module, "pkg.M")
3364 }
3365
3366 fn mutation_for_module(
3367 source: &str,
3368 source_module: &str,
3369 module: &str,
3370 ) -> Option<PythonModuleMemberMutation> {
3371 let mut parser = tree_sitter::Parser::new();
3372 parser
3373 .set_language(&tree_sitter_python::LANGUAGE.into())
3374 .expect("python grammar loads");
3375 let tree = parser.parse(source, None).expect("fixture parses");
3376 python_module_member_mutation(
3377 tree.root_node(),
3378 source,
3379 source_module,
3380 module,
3381 "NAME",
3382 4096,
3383 )
3384 }
3385
3386 #[test]
3387 #[hardwon(
3388 "#4622: writes through imported module aliases and mock patch APIs must keep a global class unknown"
3389 )]
3390 fn imported_module_member_mutation_covers_dynamic_write_forms() {
3391 for source in [
3392 "import pkg.M as target\ntarget.NAME = fake\n",
3393 "import pkg.M as target\ndel target.NAME\n",
3394 "import pkg.M as target\nsetattr(target, 'NAME', fake)\n",
3395 "import pkg.M as target\nsetattr(target, name, fake)\n",
3396 "import pkg.M as target\ntarget.__dict__['NAME'] = fake\n",
3397 "from unittest import mock\nmock.patch('pkg.M.NAME', fake)\n",
3398 "from unittest.mock import patch\npatch('pkg.M.NAME', fake)\n",
3399 "import pkg.M as target\nfrom unittest import mock\nmock.patch.object(target, 'NAME', fake)\n",
3400 ] {
3401 assert!(mutation(source).is_some(), "{source}");
3402 }
3403 assert_eq!(
3404 None,
3405 mutation("from unittest import mock\nmock.patch('pkg.Other.NAME', fake)\n")
3406 );
3407 }
3408
3409 #[test]
3410 #[hardwon("#4622: unrelated sys.modules reads blocked exact module-global class proofs")]
3411 fn sys_modules_reads_are_scoped_by_literal_module_identity() {
3412 assert_eq!(
3413 None,
3414 mutation("import sys\nother = sys.modules.get('torch')\n")
3415 );
3416 assert_eq!(None, mutation("import sys\nother = sys.modules['torch']\n"));
3417 assert!(matches!(
3418 mutation("import sys\nmodule = sys.modules.get('pkg.M')\nmodule.NAME = fake\n"),
3419 Some(PythonModuleMemberMutation::Write(_))
3420 ));
3421 assert!(matches!(
3422 mutation("import sys\nmodule = sys.modules['pkg.M']\nmodule.NAME = fake\n"),
3423 Some(PythonModuleMemberMutation::Write(_))
3424 ));
3425 assert_eq!(
3426 None,
3427 mutation("import sys\nmodule = sys.modules['pkg.M']\nconsume(module.NAME)\n")
3428 );
3429 assert!(matches!(
3430 mutation("import sys\nmodule = sys.modules[__name__]\nmodule.NAME = fake\n"),
3431 Some(PythonModuleMemberMutation::Write(_))
3432 ));
3433 assert_eq!(
3434 None,
3435 mutation_in("import sys\nmodule = sys.modules[__name__]\n", "pkg.Other")
3436 );
3437 for source in [
3438 "import sys\nsys.modules['pkg.M'] = fake\n",
3439 "import sys\nsys.modules.setdefault('pkg.M', fake)\n",
3440 "import sys\nsys.modules.update({'pkg.M': fake})\n",
3441 "import sys\nconsume(sys.modules)\n",
3442 ] {
3443 assert!(
3444 matches!(
3445 mutation(source),
3446 Some(PythonModuleMemberMutation::Escape(_))
3447 ),
3448 "{source}"
3449 );
3450 }
3451 for source in [
3452 "import sys\nmodule = sys.modules[module_name]\n",
3453 "import sys\ndel sys.modules['torch']\n",
3454 "import sys\nsys.modules.pop('torch')\n",
3455 "import sys\nsys.modules.clear()\n",
3456 "class Shim: pass\nimport sys\nsys.modules['shim'] = Shim()\n",
3457 "class Shim: pass\nimport sys\nsys.modules.update({'shim': Shim()})\n",
3458 "import sys\nimport types\nsys.modules['shim'] = types.ModuleType('shim')\n",
3459 "import sys\nsys.modules['literal'] = None\n",
3460 "import pkg.Other as shim\nimport sys\nsys.modules['shim'] = shim\n",
3461 ] {
3462 assert_eq!(mutation(source), None, "{source}");
3463 }
3464 }
3465
3466 #[test]
3467 #[hardwon("#8107: monkeypatch.setitem(sys.modules, ...) escaped every module workspace-wide")]
3468 fn sys_modules_test_helper_edits_are_table_operations() {
3469 for source in [
3470 "import sys\ndef test(monkeypatch):\n monkeypatch.setitem(sys.modules, 'dotenv', None)\n",
3471 "import sys\ndef test(monkeypatch):\n monkeypatch.delitem(sys.modules, 'dotenv', raising=False)\n",
3472 "import sys\nimport pytest\ndef test(mp: pytest.MonkeyPatch):\n mp.setitem(sys.modules, 'dotenv', None)\n",
3473 "import sys\nimport pytest\ndef test():\n mp = pytest.MonkeyPatch()\n mp.setitem(sys.modules, 'dotenv', None)\n",
3474 "import sys\nfrom unittest import mock\n@mock.patch.dict(sys.modules, {'dotenv': None})\ndef test():\n pass\n",
3475 "import sys\nfrom unittest.mock import patch\ndef test():\n with patch.dict(sys.modules, values={'dotenv': None}, clear=True, extra=None):\n pass\n",
3476 ] {
3477 assert_eq!(mutation(source), None, "{source}");
3478 }
3479 for source in [
3480 "import sys\ndef test(monkeypatch, fake):\n monkeypatch.setitem(sys.modules, 'pkg.M', fake)\n",
3482 "import pkg.M as target\nimport sys\ndef test(monkeypatch):\n monkeypatch.setitem(sys.modules, 'shim', target)\n",
3483 "import pkg.M as target\nimport sys\nfrom unittest import mock\nmock.patch.dict(sys.modules, {'shim': target})\n",
3484 "import pkg.M as target\nimport sys\nfrom unittest import mock\nmock.patch.dict(sys.modules, values={'shim': target})\n",
3485 "import pkg.M as target\nimport sys\nfrom unittest import mock\nmock.patch.dict(sys.modules, shim=target)\n",
3486 "import sys\ndef test(mp):\n mp.setitem(sys.modules, 'dotenv', None)\n",
3488 "import sys\ndef test(monkeypatch):\n monkeypatch = other\n monkeypatch.setitem(sys.modules, 'dotenv', None)\n",
3489 "import sys\ndef test(monkeypatch):\n monkeypatch.setitem(registry, sys.modules, None)\n",
3491 "import sys\ndef test(monkeypatch):\n monkeypatch.setattr(sys.modules, 'dotenv', None)\n",
3492 ] {
3493 assert!(
3494 matches!(
3495 mutation(source),
3496 Some(PythonModuleMemberMutation::Escape(_))
3497 ),
3498 "{source}"
3499 );
3500 }
3501 }
3502
3503 #[test]
3504 #[hardwon("#8114: a `with MonkeyPatch.context() as mp` receiver escaped every module")]
3505 fn sys_modules_monkeypatch_context_receiver_is_a_table_operation() {
3506 for source in [
3507 "import sys\nimport pytest\ndef test():\n with pytest.MonkeyPatch.context() as mp:\n mp.setitem(sys.modules, 'dotenv', None)\n",
3508 "import sys\nfrom pytest import MonkeyPatch\ndef test():\n with MonkeyPatch.context() as mp:\n mp.delitem(sys.modules, 'dotenv', raising=False)\n",
3509 ] {
3510 assert_eq!(mutation(source), None, "{source}");
3511 }
3512 for source in [
3513 "import pkg.M as target\nimport sys\nimport pytest\ndef test():\n with pytest.MonkeyPatch.context() as mp:\n mp.setitem(sys.modules, 'shim', target)\n",
3514 "import sys\ndef fixture(monkeypatch):\n def apply(mp=monkeypatch):\n mp.setitem(sys.modules, 'dotenv', None)\n",
3517 "import sys\ndef fixture(monkeypatch):\n def apply():\n monkeypatch.setitem(sys.modules, 'dotenv', None)\n",
3518 "import sys\nimport pytest\ndef test():\n with pytest.MonkeyPatch.context() as mp:\n mp = other\n mp.setitem(sys.modules, 'dotenv', None)\n",
3519 "import sys\ndef test():\n with other.context() as mp:\n mp.setitem(sys.modules, 'dotenv', None)\n",
3520 ] {
3521 assert!(
3522 matches!(
3523 mutation(source),
3524 Some(PythonModuleMemberMutation::Escape(_))
3525 ),
3526 "{source}"
3527 );
3528 }
3529 }
3530
3531 #[test]
3532 #[hardwon("#8115: mock.patch.dict(\"sys.modules\", ...) stored values were never checked")]
3533 fn sys_modules_patch_dict_string_target_is_a_table_update() {
3534 for source in [
3535 "from unittest import mock\nwith mock.patch.dict('sys.modules', {'dotenv': None}):\n pass\n",
3536 "from unittest import mock\n@mock.patch.dict('sys.modules', values={'dotenv': None}, clear=True)\ndef test():\n pass\n",
3537 "from unittest import mock\nmock.patch.dict(in_dict='sys.modules', dotenv=None)\n",
3538 "import sys\nfrom unittest import mock\nmock.patch.dict(in_dict=sys.modules, values={'dotenv': None})\n",
3539 "from unittest import mock\ndef test(fake):\n mock.patch.dict('os.environ', {'HOME': fake})\n",
3540 ] {
3541 assert_eq!(mutation(source), None, "{source}");
3542 }
3543 for source in [
3544 "import pkg.M as target\nfrom unittest import mock\nmock.patch.dict('sys.modules', {'alias': target})\n",
3545 "from unittest import mock\ndef test(fake):\n mock.patch.dict('sys.modules', {'alias': fake})\n",
3546 "from unittest import mock\ndef test(fake):\n mock.patch.dict('sys.modules', values={'alias': fake})\n",
3547 "from unittest import mock\ndef test(fake):\n mock.patch.dict(in_dict='sys.modules', alias=fake)\n",
3548 "from unittest import mock\ndef test(entries):\n mock.patch.dict('sys.modules', **entries)\n",
3549 "import sys\nfrom unittest import mock\ndef test(fake):\n mock.patch.dict(in_dict=sys.modules, alias=fake)\n",
3550 ] {
3551 assert!(
3552 matches!(
3553 mutation(source),
3554 Some(PythonModuleMemberMutation::Escape(_))
3555 ),
3556 "{source}"
3557 );
3558 }
3559 }
3560
3561 #[test]
3562 #[hardwon("#4622: sys.modules values must be followed by module identity")]
3563 fn dynamic_module_table_reads_follow_only_writes_through_the_possible_alias() {
3564 assert!(matches!(
3565 mutation("import pkg.M as target\nimport sys\nsys.modules['shim'] = target\n"),
3566 Some(PythonModuleMemberMutation::Escape(_))
3567 ));
3568 assert_eq!(
3569 mutation("import sys\ntarget = sys.modules[module_name]\nconsume(target.NAME)\n"),
3570 None
3571 );
3572 assert_eq!(
3573 mutation(
3574 "import sys\ntarget = sys.modules[module_name]\nconsume(getattr(target, 'NAME'))\nconsume(hasattr(target, 'NAME'))\n"
3575 ),
3576 None
3577 );
3578 assert_eq!(
3579 mutation("import sys\nconsume(sys.modules[module_name].NAME)\n"),
3580 None
3581 );
3582 assert!(matches!(
3583 mutation("import sys\ntarget = sys.modules[module_name]\ntarget.NAME = fake\n"),
3584 Some(PythonModuleMemberMutation::Write(_))
3585 ));
3586 }
3587
3588 #[test]
3589 #[hardwon("#4622: a dominating string filter excludes unrelated dynamic sys.modules aliases")]
3590 fn dynamic_sys_modules_keys_respect_dominating_string_filters() {
3591 assert_eq!(
3592 mutation_for_module(
3593 "import sys\ndef patch():\n for module_name in module_names:\n if 'huggingface' in module_name or module_name.startswith('tqdm'):\n target = sys.modules[module_name]\n target.NAME = fake\n",
3594 "pkg.M",
3595 "backend.backends",
3596 ),
3597 None
3598 );
3599 assert!(matches!(
3600 mutation_for_module(
3601 "import sys\ndef patch():\n for module_name in module_names:\n if module_name.startswith('backend'):\n target = sys.modules[module_name]\n target.NAME = fake\n",
3602 "pkg.M",
3603 "backend.backends",
3604 ),
3605 Some(PythonModuleMemberMutation::Write(_))
3606 ));
3607 assert!(matches!(
3608 mutation_for_module(
3609 "import sys\ndef patch(allowed):\n for module_name in module_names:\n if module_name in allowed:\n target = sys.modules[module_name]\n target.NAME = fake\n",
3610 "pkg.M",
3611 "backend.backends",
3612 ),
3613 Some(PythonModuleMemberMutation::Write(_))
3614 ));
3615 assert!(matches!(
3616 mutation_for_module(
3617 "import sys\ndef patch():\n for module_name in module_names:\n if 'huggingface' in module_name:\n module_name = 'backend.backends'\n target = sys.modules[module_name]\n target.NAME = fake\n",
3618 "pkg.M",
3619 "backend.backends",
3620 ),
3621 Some(PythonModuleMemberMutation::Write(_))
3622 ));
3623 assert_eq!(
3624 mutation_for_module(
3625 "import sys\ndef patch():\n for module_name in module_names:\n if not ('huggingface' in module_name):\n continue\n target = sys.modules[module_name]\n target.NAME = fake\n",
3626 "pkg.M",
3627 "backend.backends",
3628 ),
3629 None
3630 );
3631 }
3632}