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